mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	LTR-303, LTR-329, LTR-553, LTR-556, LTR-559, LTR-659 Series of Lite-On Light (ALS) and Proximity(PS) sensors (#6076)
* LTR303 and LTR329 light sensors * LTR303 tidy up * LTR303 unused var * LTR303 tidy up + test * LTR303 auto sensitivity mode * LTR303 auto sensitivity mode tidy * LTR303 State machine version * LTR303 name fix * publish split * minor * new definitions for LTR * als-ps test * als-ps test * als-ps test * ps options * ps options * trgger bug fixed * trgger bug fixed * Minor comments * ltr303->ltr_als_ps * codeowners, tests * tidy up * tidy up * tidy up * gain enum name fix * auto gain fix * tweaks * new style tests * als/ps separate init * logd->logv * reconfiguration count changed * old-style tests removed * const py * ambient light const in vmel7700 and ltr390 * Update esphome/components/ltr_als_ps/ltr_als_ps.cpp Co-authored-by: Keith Burzinski <kbx81x@gmail.com> * Apply suggestions from code review Co-authored-by: Keith Burzinski <kbx81x@gmail.com> * remove commented code --------- Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
		
							
								
								
									
										1
									
								
								esphome/components/ltr_als_ps/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/ltr_als_ps/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@latonita"] | ||||
							
								
								
									
										519
									
								
								esphome/components/ltr_als_ps/ltr_als_ps.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										519
									
								
								esphome/components/ltr_als_ps/ltr_als_ps.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,519 @@ | ||||
| #include "ltr_als_ps.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| using esphome::i2c::ErrorCode; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ltr_als_ps { | ||||
|  | ||||
| static const char *const TAG = "ltr_als_ps"; | ||||
|  | ||||
| static const uint8_t MAX_TRIES = 5; | ||||
|  | ||||
| template<typename T, size_t size> T get_next(const T (&array)[size], const T val) { | ||||
|   size_t i = 0; | ||||
|   size_t idx = -1; | ||||
|   while (idx == -1 && i < size) { | ||||
|     if (array[i] == val) { | ||||
|       idx = i; | ||||
|       break; | ||||
|     } | ||||
|     i++; | ||||
|   } | ||||
|   if (idx == -1 || i + 1 >= size) | ||||
|     return val; | ||||
|   return array[i + 1]; | ||||
| } | ||||
|  | ||||
| template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { | ||||
|   size_t i = size - 1; | ||||
|   size_t idx = -1; | ||||
|   while (idx == -1 && i > 0) { | ||||
|     if (array[i] == val) { | ||||
|       idx = i; | ||||
|       break; | ||||
|     } | ||||
|     i--; | ||||
|   } | ||||
|   if (idx == -1 || i == 0) | ||||
|     return val; | ||||
|   return array[i - 1]; | ||||
| } | ||||
|  | ||||
| static uint16_t get_itime_ms(IntegrationTime time) { | ||||
|   static const uint16_t ALS_INT_TIME[8] = {100, 50, 200, 400, 150, 250, 300, 350}; | ||||
|   return ALS_INT_TIME[time & 0b111]; | ||||
| } | ||||
|  | ||||
| static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) { | ||||
|   static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000}; | ||||
|   return ALS_MEAS_RATE[rate & 0b111]; | ||||
| } | ||||
|  | ||||
| static float get_gain_coeff(AlsGain gain) { | ||||
|   static const float ALS_GAIN[8] = {1, 2, 4, 8, 0, 0, 48, 96}; | ||||
|   return ALS_GAIN[gain & 0b111]; | ||||
| } | ||||
|  | ||||
| static float get_ps_gain_coeff(PsGain gain) { | ||||
|   static const float PS_GAIN[4] = {16, 0, 32, 64}; | ||||
|   return PS_GAIN[gain & 0b11]; | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up LTR-303/329/55x/659"); | ||||
|   // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive | ||||
|   this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; }); | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::dump_config() { | ||||
|   auto get_device_type = [](LtrType typ) { | ||||
|     switch (typ) { | ||||
|       case LtrType::LTR_TYPE_ALS_ONLY: | ||||
|         return "ALS only"; | ||||
|       case LtrType::LTR_TYPE_PS_ONLY: | ||||
|         return "PS only"; | ||||
|       case LtrType::LTR_TYPE_ALS_AND_PS: | ||||
|         return "ALS + PS"; | ||||
|       default: | ||||
|         return "Unknown"; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   ESP_LOGCONFIG(TAG, "  Device type: %s", get_device_type(this->ltr_type_)); | ||||
|   if (this->is_als_()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Automatic mode: %s", ONOFF(this->automatic_mode_enabled_)); | ||||
|     ESP_LOGCONFIG(TAG, "  Gain: %.0fx", get_gain_coeff(this->gain_)); | ||||
|     ESP_LOGCONFIG(TAG, "  Integration time: %d ms", get_itime_ms(this->integration_time_)); | ||||
|     ESP_LOGCONFIG(TAG, "  Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_)); | ||||
|     ESP_LOGCONFIG(TAG, "  Glass attenuation factor: %f", this->glass_attenuation_factor_); | ||||
|     LOG_SENSOR("  ", "ALS calculated lux", this->ambient_light_sensor_); | ||||
|     LOG_SENSOR("  ", "CH1 Infrared counts", this->infrared_counts_sensor_); | ||||
|     LOG_SENSOR("  ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_); | ||||
|     LOG_SENSOR("  ", "Actual gain", this->actual_gain_sensor_); | ||||
|   } | ||||
|   if (this->is_ps_()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_)); | ||||
|     ESP_LOGCONFIG(TAG, "  Proximity cooldown time: %d s", this->ps_cooldown_time_s_); | ||||
|     ESP_LOGCONFIG(TAG, "  Proximity high threshold: %d", this->ps_threshold_high_); | ||||
|     ESP_LOGCONFIG(TAG, "  Proximity low threshold: %d", this->ps_threshold_low_); | ||||
|     LOG_SENSOR("  ", "Proximity counts", this->proximity_counts_sensor_); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with I2C LTR-303/329/55x/659 failed!"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::update() { | ||||
|   ESP_LOGV(TAG, "Updating"); | ||||
|   if (this->is_ready() && this->state_ == State::IDLE) { | ||||
|     ESP_LOGV(TAG, "Initiating new data collection"); | ||||
|  | ||||
|     this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::WAITING_FOR_DATA; | ||||
|  | ||||
|     this->als_readings_.ch0 = 0; | ||||
|     this->als_readings_.ch1 = 0; | ||||
|     this->als_readings_.gain = this->gain_; | ||||
|     this->als_readings_.integration_time = this->integration_time_; | ||||
|     this->als_readings_.lux = 0; | ||||
|     this->als_readings_.number_of_adjustments = 0; | ||||
|  | ||||
|   } else { | ||||
|     ESP_LOGV(TAG, "Component not ready yet"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::loop() { | ||||
|   ErrorCode err = i2c::ERROR_OK; | ||||
|   static uint8_t tries{0}; | ||||
|  | ||||
|   switch (this->state_) { | ||||
|     case State::DELAYED_SETUP: | ||||
|       err = this->write(nullptr, 0); | ||||
|       if (err != i2c::ERROR_OK) { | ||||
|         ESP_LOGV(TAG, "i2c connection failed"); | ||||
|         this->mark_failed(); | ||||
|       } | ||||
|       this->configure_reset_(); | ||||
|       if (this->is_als_()) { | ||||
|         this->configure_als_(); | ||||
|         this->configure_integration_time_(this->integration_time_); | ||||
|       } | ||||
|       if (this->is_ps_()) { | ||||
|         this->configure_ps_(); | ||||
|       } | ||||
|  | ||||
|       this->state_ = State::IDLE; | ||||
|       break; | ||||
|  | ||||
|     case State::IDLE: | ||||
|       if (this->is_ps_()) { | ||||
|         check_and_trigger_ps_(); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case State::WAITING_FOR_DATA: | ||||
|       if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { | ||||
|         tries = 0; | ||||
|         ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), | ||||
|                  get_itime_ms(this->als_readings_.integration_time)); | ||||
|         this->read_sensor_data_(this->als_readings_); | ||||
|         this->state_ = State::DATA_COLLECTED; | ||||
|         this->apply_lux_calculation_(this->als_readings_); | ||||
|       } else if (tries >= MAX_TRIES) { | ||||
|         ESP_LOGW(TAG, "Can't get data after several tries."); | ||||
|         tries = 0; | ||||
|         this->status_set_warning(); | ||||
|         this->state_ = State::IDLE; | ||||
|         return; | ||||
|       } else { | ||||
|         tries++; | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case State::COLLECTING_DATA_AUTO: | ||||
|     case State::DATA_COLLECTED: | ||||
|       // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration | ||||
|       if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) { | ||||
|         this->state_ = State::ADJUSTMENT_IN_PROGRESS; | ||||
|         ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), | ||||
|                  get_itime_ms(this->als_readings_.integration_time)); | ||||
|         this->configure_integration_time_(this->als_readings_.integration_time); | ||||
|         this->configure_gain_(this->als_readings_.gain); | ||||
|         // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters | ||||
|         this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_), | ||||
|                           [this]() { this->state_ = State::WAITING_FOR_DATA; }); | ||||
|       } else { | ||||
|         this->state_ = State::READY_TO_PUBLISH; | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case State::ADJUSTMENT_IN_PROGRESS: | ||||
|       // nothing to be done, just waiting for the timeout | ||||
|       break; | ||||
|  | ||||
|     case State::READY_TO_PUBLISH: | ||||
|       this->publish_data_part_1_(this->als_readings_); | ||||
|       this->state_ = State::KEEP_PUBLISHING; | ||||
|       break; | ||||
|  | ||||
|     case State::KEEP_PUBLISHING: | ||||
|       this->publish_data_part_2_(this->als_readings_); | ||||
|       this->status_clear_warning(); | ||||
|       this->state_ = State::IDLE; | ||||
|       break; | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::check_and_trigger_ps_() { | ||||
|   static uint32_t last_high_trigger_time{0}; | ||||
|   static uint32_t last_low_trigger_time{0}; | ||||
|   uint16_t ps_data = this->read_ps_data_(); | ||||
|   uint32_t now = millis(); | ||||
|  | ||||
|   if (ps_data != this->ps_readings_) { | ||||
|     this->ps_readings_ = ps_data; | ||||
|     // Higher values - object is closer to sensor | ||||
|     if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { | ||||
|       last_high_trigger_time = now; | ||||
|       ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, | ||||
|                this->ps_threshold_high_); | ||||
|       this->on_ps_high_trigger_callback_.call(); | ||||
|     } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { | ||||
|       last_low_trigger_time = now; | ||||
|       ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, | ||||
|                this->ps_threshold_low_); | ||||
|       this->on_ps_low_trigger_callback_.call(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool LTRAlsPsComponent::check_part_number_() { | ||||
|   uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get(); | ||||
|   if (manuf_id != 0x05) {  // 0x05 is Lite-On Semiconductor Corp. ID | ||||
|     ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id); | ||||
|     this->mark_failed(); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   // Things getting not really funny here, we can't identify device type by part number ID | ||||
|   // ======================== ========= ===== ================= | ||||
|   // Device                    Part ID   Rev   Capabilities | ||||
|   // ======================== ========= ===== ================= | ||||
|   // Ltr-329/ltr-303            0x0a    0x00  Als 16b | ||||
|   // Ltr-553/ltr-556/ltr-556    0x09    0x02  Als 16b + Ps 11b  diff nm sens | ||||
|   // Ltr-659                    0x09    0x02  Ps 11b and ps gain | ||||
|   // | ||||
|   // There are other devices which might potentially work with default settings, | ||||
|   // but registers layout is different and we can't use them properly. For ex. ltr-558 | ||||
|  | ||||
|   PartIdRegister part_id{0}; | ||||
|   part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get(); | ||||
|   if (part_id.part_number_id != 0x0a && part_id.part_number_id != 0x09) { | ||||
|     ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. It might not work properly.", part_id.part_number_id); | ||||
|     this->status_set_warning(); | ||||
|     return true; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::configure_reset_() { | ||||
|   ESP_LOGV(TAG, "Resetting"); | ||||
|  | ||||
|   AlsControlRegister als_ctrl{0}; | ||||
|   als_ctrl.sw_reset = true; | ||||
|   this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; | ||||
|   delay(2); | ||||
|  | ||||
|   uint8_t tries = MAX_TRIES; | ||||
|   do { | ||||
|     ESP_LOGV(TAG, "Waiting for chip to reset"); | ||||
|     delay(2); | ||||
|     als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); | ||||
|   } while (als_ctrl.sw_reset && tries--);  // while sw reset bit is on - keep waiting | ||||
|  | ||||
|   if (als_ctrl.sw_reset) { | ||||
|     ESP_LOGW(TAG, "Reset timed out"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::configure_als_() { | ||||
|   AlsControlRegister als_ctrl{0}; | ||||
|  | ||||
|   als_ctrl.sw_reset = false; | ||||
|   als_ctrl.active_mode = true; | ||||
|   als_ctrl.gain = this->gain_; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw); | ||||
|   this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; | ||||
|   delay(5); | ||||
|  | ||||
|   uint8_t tries = MAX_TRIES; | ||||
|   do { | ||||
|     ESP_LOGV(TAG, "Waiting for device to become active..."); | ||||
|     delay(2); | ||||
|     als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); | ||||
|   } while (!als_ctrl.active_mode && tries--);  // while active mode is not set - keep waiting | ||||
|  | ||||
|   if (!als_ctrl.active_mode) { | ||||
|     ESP_LOGW(TAG, "Failed to activate device"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::configure_ps_() { | ||||
|   PsMeasurementRateRegister ps_meas{0}; | ||||
|   ps_meas.ps_measurement_rate = PsMeasurementRate::PS_MEAS_RATE_50MS; | ||||
|   this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw; | ||||
|  | ||||
|   PsControlRegister ps_ctrl{0}; | ||||
|   ps_ctrl.ps_mode_active = true; | ||||
|   ps_ctrl.ps_mode_xxx = true; | ||||
|   this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw; | ||||
| } | ||||
|  | ||||
| uint16_t LTRAlsPsComponent::read_ps_data_() { | ||||
|   AlsPsStatusRegister als_status{0}; | ||||
|   als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); | ||||
|   if (!als_status.ps_new_data || als_status.data_invalid) { | ||||
|     return this->ps_readings_; | ||||
|   } | ||||
|  | ||||
|   uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get(); | ||||
|   PsData1Register ps_high; | ||||
|   ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get(); | ||||
|  | ||||
|   uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low); | ||||
|   if (ps_high.ps_saturation_flag) { | ||||
|     return 0x7ff;  // full 11 bit range | ||||
|   } | ||||
|   return val; | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::configure_gain_(AlsGain gain) { | ||||
|   AlsControlRegister als_ctrl{0}; | ||||
|   als_ctrl.active_mode = true; | ||||
|   als_ctrl.gain = gain; | ||||
|   this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; | ||||
|   delay(2); | ||||
|  | ||||
|   AlsControlRegister read_als_ctrl{0}; | ||||
|   read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); | ||||
|   if (read_als_ctrl.gain != gain) { | ||||
|     ESP_LOGW(TAG, "Failed to set gain. We will try one more time."); | ||||
|     this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; | ||||
|     delay(2); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { | ||||
|   MeasurementRateRegister meas{0}; | ||||
|   meas.measurement_repeat_rate = this->repeat_rate_; | ||||
|   meas.integration_time = time; | ||||
|   this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; | ||||
|   delay(2); | ||||
|  | ||||
|   MeasurementRateRegister read_meas{0}; | ||||
|   read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get(); | ||||
|   if (read_meas.integration_time != time) { | ||||
|     ESP_LOGW(TAG, "Failed to set integration time. We will try one more time."); | ||||
|     this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; | ||||
|     delay(2); | ||||
|   } | ||||
| } | ||||
|  | ||||
| DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { | ||||
|   AlsPsStatusRegister als_status{0}; | ||||
|  | ||||
|   als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); | ||||
|   if (!als_status.als_new_data) | ||||
|     return DataAvail::NO_DATA; | ||||
|  | ||||
|   if (als_status.data_invalid) { | ||||
|     ESP_LOGW(TAG, "Data available but not valid"); | ||||
|     return DataAvail::BAD_DATA; | ||||
|   } | ||||
|   ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain)); | ||||
|   if (data.gain != als_status.gain) { | ||||
|     ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); | ||||
|     return DataAvail::BAD_DATA; | ||||
|   } | ||||
|   return DataAvail::DATA_OK; | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { | ||||
|   data.ch1 = 0; | ||||
|   data.ch0 = 0; | ||||
|   uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get(); | ||||
|   uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get(); | ||||
|   uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get(); | ||||
|   uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get(); | ||||
|   data.ch1 = encode_uint16(ch1_1, ch1_0); | ||||
|   data.ch0 = encode_uint16(ch0_1, ch0_0); | ||||
|  | ||||
|   ESP_LOGV(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0); | ||||
| } | ||||
|  | ||||
| bool LTRAlsPsComponent::are_adjustments_required_(AlsReadings &data) { | ||||
|   if (!this->automatic_mode_enabled_) | ||||
|     return false; | ||||
|  | ||||
|   if (data.number_of_adjustments > 15) { | ||||
|     // sometimes sensors fail to change sensitivity. this prevents us from infinite loop | ||||
|     ESP_LOGW(TAG, "Too many sensitivity adjustments done. Apparently, sensor reconfiguration fails. Stopping."); | ||||
|     return false; | ||||
|   } | ||||
|   data.number_of_adjustments++; | ||||
|  | ||||
|   // Recommended thresholds as per datasheet | ||||
|   static const uint16_t LOW_INTENSITY_THRESHOLD = 1000; | ||||
|   static const uint16_t HIGH_INTENSITY_THRESHOLD = 30000; | ||||
|   static const AlsGain GAINS[GAINS_COUNT] = {GAIN_1, GAIN_2, GAIN_4, GAIN_8, GAIN_48, GAIN_96}; | ||||
|   static const IntegrationTime INT_TIMES[TIMES_COUNT] = { | ||||
|       INTEGRATION_TIME_50MS,  INTEGRATION_TIME_100MS, INTEGRATION_TIME_150MS, INTEGRATION_TIME_200MS, | ||||
|       INTEGRATION_TIME_250MS, INTEGRATION_TIME_300MS, INTEGRATION_TIME_350MS, INTEGRATION_TIME_400MS}; | ||||
|  | ||||
|   if (data.ch0 <= LOW_INTENSITY_THRESHOLD) { | ||||
|     AlsGain next_gain = get_next(GAINS, data.gain); | ||||
|     if (next_gain != data.gain) { | ||||
|       data.gain = next_gain; | ||||
|       ESP_LOGV(TAG, "Low illuminance. Increasing gain."); | ||||
|       return true; | ||||
|     } | ||||
|     IntegrationTime next_time = get_next(INT_TIMES, data.integration_time); | ||||
|     if (next_time != data.integration_time) { | ||||
|       data.integration_time = next_time; | ||||
|       ESP_LOGV(TAG, "Low illuminance. Increasing integration time."); | ||||
|       return true; | ||||
|     } | ||||
|   } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD) { | ||||
|     AlsGain prev_gain = get_prev(GAINS, data.gain); | ||||
|     if (prev_gain != data.gain) { | ||||
|       data.gain = prev_gain; | ||||
|       ESP_LOGV(TAG, "High illuminance. Decreasing gain."); | ||||
|       return true; | ||||
|     } | ||||
|     IntegrationTime prev_time = get_prev(INT_TIMES, data.integration_time); | ||||
|     if (prev_time != data.integration_time) { | ||||
|       data.integration_time = prev_time; | ||||
|       ESP_LOGV(TAG, "High illuminance. Decreasing integration time."); | ||||
|       return true; | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "Illuminance is sufficient."); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Can't adjust sensitivity anymore."); | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::apply_lux_calculation_(AlsReadings &data) { | ||||
|   if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) { | ||||
|     ESP_LOGW(TAG, "Sensors got saturated"); | ||||
|     data.lux = 0.0f; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) { | ||||
|     ESP_LOGW(TAG, "Sensors blacked out"); | ||||
|     data.lux = 0.0f; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   float ch0 = data.ch0; | ||||
|   float ch1 = data.ch1; | ||||
|   float ratio = ch1 / (ch0 + ch1); | ||||
|   float als_gain = get_gain_coeff(data.gain); | ||||
|   float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f; | ||||
|   float inv_pfactor = this->glass_attenuation_factor_; | ||||
|   float lux = 0.0f; | ||||
|  | ||||
|   if (ratio < 0.45) { | ||||
|     lux = (1.7743 * ch0 + 1.1059 * ch1); | ||||
|   } else if (ratio < 0.64 && ratio >= 0.45) { | ||||
|     lux = (4.2785 * ch0 - 1.9548 * ch1); | ||||
|   } else if (ratio < 0.85 && ratio >= 0.64) { | ||||
|     lux = (0.5926 * ch0 + 0.1185 * ch1); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio"); | ||||
|     lux = 0.0f; | ||||
|   } | ||||
|   lux = inv_pfactor * lux / als_gain / als_time; | ||||
|   data.lux = lux; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain, | ||||
|            als_time, inv_pfactor, lux); | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::publish_data_part_1_(AlsReadings &data) { | ||||
|   if (this->proximity_counts_sensor_ != nullptr) { | ||||
|     this->proximity_counts_sensor_->publish_state(this->ps_readings_); | ||||
|   } | ||||
|   if (this->ambient_light_sensor_ != nullptr) { | ||||
|     this->ambient_light_sensor_->publish_state(data.lux); | ||||
|   } | ||||
|   if (this->infrared_counts_sensor_ != nullptr) { | ||||
|     this->infrared_counts_sensor_->publish_state(data.ch1); | ||||
|   } | ||||
|   if (this->full_spectrum_counts_sensor_ != nullptr) { | ||||
|     this->full_spectrum_counts_sensor_->publish_state(data.ch0); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LTRAlsPsComponent::publish_data_part_2_(AlsReadings &data) { | ||||
|   if (this->actual_gain_sensor_ != nullptr) { | ||||
|     this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain)); | ||||
|   } | ||||
|   if (this->actual_integration_time_sensor_ != nullptr) { | ||||
|     this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time)); | ||||
|   } | ||||
| } | ||||
| }  // namespace ltr_als_ps | ||||
| }  // namespace esphome | ||||
							
								
								
									
										184
									
								
								esphome/components/ltr_als_ps/ltr_als_ps.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								esphome/components/ltr_als_ps/ltr_als_ps.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/optional.h" | ||||
| #include "esphome/core/automation.h" | ||||
|  | ||||
| #include "ltr_definitions.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ltr_als_ps { | ||||
|  | ||||
| enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; | ||||
|  | ||||
| enum LtrType : uint8_t { | ||||
|   LTR_TYPE_UNKNOWN = 0, | ||||
|   LTR_TYPE_ALS_ONLY = 1, | ||||
|   LTR_TYPE_PS_ONLY = 2, | ||||
|   LTR_TYPE_ALS_AND_PS = 3, | ||||
| }; | ||||
|  | ||||
| class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   // | ||||
|   // EspHome framework functions | ||||
|   // | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|   void loop() override; | ||||
|  | ||||
|   // Configuration setters : General | ||||
|   // | ||||
|   void set_ltr_type(LtrType type) { this->ltr_type_ = type; } | ||||
|  | ||||
|   // Configuration setters : ALS | ||||
|   // | ||||
|   void set_als_auto_mode(bool enable) { this->automatic_mode_enabled_ = enable; } | ||||
|   void set_als_gain(AlsGain gain) { this->gain_ = gain; } | ||||
|   void set_als_integration_time(IntegrationTime time) { this->integration_time_ = time; } | ||||
|   void set_als_meas_repeat_rate(MeasurementRepeatRate rate) { this->repeat_rate_ = rate; } | ||||
|   void set_als_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; } | ||||
|  | ||||
|   // Configuration setters : PS | ||||
|   // | ||||
|   void set_ps_high_threshold(uint16_t threshold) { this->ps_threshold_high_ = threshold; } | ||||
|   void set_ps_low_threshold(uint16_t threshold) { this->ps_threshold_low_ = threshold; } | ||||
|   void set_ps_cooldown_time_s(uint16_t time) { this->ps_cooldown_time_s_ = time; } | ||||
|   void set_ps_gain(PsGain gain) { this->ps_gain_ = gain; } | ||||
|  | ||||
|   // Sensors setters | ||||
|   // | ||||
|   void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; } | ||||
|   void set_full_spectrum_counts_sensor(sensor::Sensor *sensor) { this->full_spectrum_counts_sensor_ = sensor; } | ||||
|   void set_infrared_counts_sensor(sensor::Sensor *sensor) { this->infrared_counts_sensor_ = sensor; } | ||||
|   void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; } | ||||
|   void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } | ||||
|   void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } | ||||
|  | ||||
|  protected: | ||||
|   // | ||||
|   // Internal state machine, used to split all the actions into | ||||
|   // small steps in loop() to make sure we are not blocking execution | ||||
|   // | ||||
|   enum class State : uint8_t { | ||||
|     NOT_INITIALIZED, | ||||
|     DELAYED_SETUP, | ||||
|     IDLE, | ||||
|     WAITING_FOR_DATA, | ||||
|     COLLECTING_DATA_AUTO, | ||||
|     DATA_COLLECTED, | ||||
|     ADJUSTMENT_IN_PROGRESS, | ||||
|     READY_TO_PUBLISH, | ||||
|     KEEP_PUBLISHING | ||||
|   } state_{State::NOT_INITIALIZED}; | ||||
|  | ||||
|   LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; | ||||
|  | ||||
|   // | ||||
|   // Current measurements data | ||||
|   // | ||||
|   struct AlsReadings { | ||||
|     uint16_t ch0{0}; | ||||
|     uint16_t ch1{0}; | ||||
|     AlsGain gain{AlsGain::GAIN_1}; | ||||
|     IntegrationTime integration_time{IntegrationTime::INTEGRATION_TIME_100MS}; | ||||
|     float lux{0.0f}; | ||||
|     uint8_t number_of_adjustments{0}; | ||||
|   } als_readings_; | ||||
|   uint16_t ps_readings_{0xfffe}; | ||||
|  | ||||
|   inline bool is_als_() const { | ||||
|     return this->ltr_type_ == LtrType::LTR_TYPE_ALS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; | ||||
|   } | ||||
|   inline bool is_ps_() const { | ||||
|     return this->ltr_type_ == LtrType::LTR_TYPE_PS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; | ||||
|   } | ||||
|  | ||||
|   // | ||||
|   // Device interaction and data manipulation | ||||
|   // | ||||
|   bool check_part_number_(); | ||||
|  | ||||
|   void configure_reset_(); | ||||
|   void configure_als_(); | ||||
|   void configure_integration_time_(IntegrationTime time); | ||||
|   void configure_gain_(AlsGain gain); | ||||
|   DataAvail is_als_data_ready_(AlsReadings &data); | ||||
|   void read_sensor_data_(AlsReadings &data); | ||||
|   bool are_adjustments_required_(AlsReadings &data); | ||||
|   void apply_lux_calculation_(AlsReadings &data); | ||||
|   void publish_data_part_1_(AlsReadings &data); | ||||
|   void publish_data_part_2_(AlsReadings &data); | ||||
|  | ||||
|   void configure_ps_(); | ||||
|   uint16_t read_ps_data_(); | ||||
|   void check_and_trigger_ps_(); | ||||
|  | ||||
|   // | ||||
|   // Component configuration | ||||
|   // | ||||
|   bool automatic_mode_enabled_{true}; | ||||
|   AlsGain gain_{AlsGain::GAIN_1}; | ||||
|   IntegrationTime integration_time_{IntegrationTime::INTEGRATION_TIME_100MS}; | ||||
|   MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS}; | ||||
|   float glass_attenuation_factor_{1.0}; | ||||
|  | ||||
|   uint16_t ps_cooldown_time_s_{5}; | ||||
|   PsGain ps_gain_{PsGain::PS_GAIN_16}; | ||||
|   uint16_t ps_threshold_high_{0xffff}; | ||||
|   uint16_t ps_threshold_low_{0x0000}; | ||||
|  | ||||
|   // | ||||
|   //   Sensors for publishing data | ||||
|   // | ||||
|   sensor::Sensor *infrared_counts_sensor_{nullptr};          // direct reading CH1, infrared only | ||||
|   sensor::Sensor *full_spectrum_counts_sensor_{nullptr};     // direct reading CH0, infrared + visible light | ||||
|   sensor::Sensor *ambient_light_sensor_{nullptr};            // calculated lux | ||||
|   sensor::Sensor *actual_gain_sensor_{nullptr};              // actual gain of reading | ||||
|   sensor::Sensor *actual_integration_time_sensor_{nullptr};  // actual integration time | ||||
|   sensor::Sensor *proximity_counts_sensor_{nullptr};         // proximity sensor | ||||
|  | ||||
|   bool is_any_als_sensor_enabled_() const { | ||||
|     return this->ambient_light_sensor_ != nullptr || this->full_spectrum_counts_sensor_ != nullptr || | ||||
|            this->infrared_counts_sensor_ != nullptr || this->actual_gain_sensor_ != nullptr || | ||||
|            this->actual_integration_time_sensor_ != nullptr; | ||||
|   } | ||||
|   bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } | ||||
|  | ||||
|   // | ||||
|   // Trigger section for the automations | ||||
|   // | ||||
|   friend class LTRPsHighTrigger; | ||||
|   friend class LTRPsLowTrigger; | ||||
|  | ||||
|   CallbackManager<void()> on_ps_high_trigger_callback_; | ||||
|   CallbackManager<void()> on_ps_low_trigger_callback_; | ||||
|  | ||||
|   void add_on_ps_high_trigger_callback_(std::function<void()> callback) { | ||||
|     this->on_ps_high_trigger_callback_.add(std::move(callback)); | ||||
|   } | ||||
|  | ||||
|   void add_on_ps_low_trigger_callback_(std::function<void()> callback) { | ||||
|     this->on_ps_low_trigger_callback_.add(std::move(callback)); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class LTRPsHighTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit LTRPsHighTrigger(LTRAlsPsComponent *parent) { | ||||
|     parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class LTRPsLowTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit LTRPsLowTrigger(LTRAlsPsComponent *parent) { | ||||
|     parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); | ||||
|   } | ||||
| }; | ||||
| }  // namespace ltr_als_ps | ||||
| }  // namespace esphome | ||||
							
								
								
									
										275
									
								
								esphome/components/ltr_als_ps/ltr_definitions.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								esphome/components/ltr_als_ps/ltr_definitions.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ltr_als_ps { | ||||
|  | ||||
| enum class CommandRegisters : uint8_t { | ||||
|   ALS_CONTR = 0x80,         // ALS operation mode control and SW reset | ||||
|   PS_CONTR = 0x81,          // PS operation mode control | ||||
|   PS_LED = 0x82,            // PS LED pulse frequency control | ||||
|   PS_N_PULSES = 0x83,       // PS number of pulses control | ||||
|   PS_MEAS_RATE = 0x84,      // PS measurement rate in active mode | ||||
|   MEAS_RATE = 0x85,         // ALS measurement rate in active mode | ||||
|   PART_ID = 0x86,           // Part Number ID and Revision ID | ||||
|   MANUFAC_ID = 0x87,        // Manufacturer ID | ||||
|   ALS_DATA_CH1_0 = 0x88,    // ALS measurement CH1 data, lower byte - infrared only | ||||
|   ALS_DATA_CH1_1 = 0x89,    // ALS measurement CH1 data, upper byte - infrared only | ||||
|   ALS_DATA_CH0_0 = 0x8A,    // ALS measurement CH0 data, lower byte - visible + infrared | ||||
|   ALS_DATA_CH0_1 = 0x8B,    // ALS measurement CH0 data, upper byte - visible + infrared | ||||
|   ALS_PS_STATUS = 0x8C,     // ALS PS new data status | ||||
|   PS_DATA_0 = 0x8D,         // PS measurement data, lower byte | ||||
|   PS_DATA_1 = 0x8E,         // PS measurement data, upper byte | ||||
|   ALS_PS_INTERRUPT = 0x8F,  // Interrupt status | ||||
|   PS_THRES_UP_0 = 0x90,     // PS interrupt upper threshold, lower byte | ||||
|   PS_THRES_UP_1 = 0x91,     // PS interrupt upper threshold, upper byte | ||||
|   PS_THRES_LOW_0 = 0x92,    // PS interrupt lower threshold, lower byte | ||||
|   PS_THRES_LOW_1 = 0x93,    // PS interrupt lower threshold, upper byte | ||||
|   PS_OFFSET_1 = 0x94,       // PS offset, upper byte | ||||
|   PS_OFFSET_0 = 0x95,       // PS offset, lower byte | ||||
|                             // 0x96 - reserved | ||||
|   ALS_THRES_UP_0 = 0x97,    // ALS interrupt upper threshold, lower byte | ||||
|   ALS_THRES_UP_1 = 0x98,    // ALS interrupt upper threshold, upper byte | ||||
|   ALS_THRES_LOW_0 = 0x99,   // ALS interrupt lower threshold, lower byte | ||||
|   ALS_THRES_LOW_1 = 0x9A,   // ALS interrupt lower threshold, upper byte | ||||
|                             // 0x9B - reserved | ||||
|                             // 0x9C - reserved | ||||
|                             // 0x9D - reserved | ||||
|   INTERRUPT_PERSIST = 0x9E  // Interrupt persistence filter | ||||
| }; | ||||
|  | ||||
| // ALS Sensor gain levels | ||||
| enum AlsGain : uint8_t { | ||||
|   GAIN_1 = 0,  // default | ||||
|   GAIN_2 = 1, | ||||
|   GAIN_4 = 2, | ||||
|   GAIN_8 = 3, | ||||
|   GAIN_48 = 6, | ||||
|   GAIN_96 = 7, | ||||
| }; | ||||
| static const uint8_t GAINS_COUNT = 6; | ||||
|  | ||||
| // ALS Sensor integration times | ||||
| enum IntegrationTime : uint8_t { | ||||
|   INTEGRATION_TIME_100MS = 0,  // default | ||||
|   INTEGRATION_TIME_50MS = 1, | ||||
|   INTEGRATION_TIME_200MS = 2, | ||||
|   INTEGRATION_TIME_400MS = 3, | ||||
|   INTEGRATION_TIME_150MS = 4, | ||||
|   INTEGRATION_TIME_250MS = 5, | ||||
|   INTEGRATION_TIME_300MS = 6, | ||||
|   INTEGRATION_TIME_350MS = 7 | ||||
| }; | ||||
| static const uint8_t TIMES_COUNT = 8; | ||||
|  | ||||
| // ALS Sensor measurement repeat rate | ||||
| enum MeasurementRepeatRate { | ||||
|   REPEAT_RATE_50MS = 0, | ||||
|   REPEAT_RATE_100MS = 1, | ||||
|   REPEAT_RATE_200MS = 2, | ||||
|   REPEAT_RATE_500MS = 3,  // default | ||||
|   REPEAT_RATE_1000MS = 4, | ||||
|   REPEAT_RATE_2000MS = 5 | ||||
| }; | ||||
|  | ||||
| // PS Sensor gain levels | ||||
| enum PsGain : uint8_t { | ||||
|   PS_GAIN_16 = 0,  // default | ||||
|   PS_GAIN_32 = 2, | ||||
|   PS_GAIN_64 = 3, | ||||
| }; | ||||
|  | ||||
| // PS Mode | ||||
| enum PsMode : uint8_t { | ||||
|   PS_MODE_STANDBY_00 = 0,  // default | ||||
|   PS_MODE_STANDBY_01 = 1, | ||||
|   PS_MODE_ACTIVE_10 = 2, | ||||
|   PS_MODE_ACTIVE_11 = 3, | ||||
| }; | ||||
|  | ||||
| // LED Pulse Modulation Frequency | ||||
| enum PsLedFreq : uint8_t { | ||||
|   PS_LED_FREQ_30KHZ = 0, | ||||
|   PS_LED_FREQ_40KHZ = 1, | ||||
|   PS_LED_FREQ_50KHZ = 2, | ||||
|   PS_LED_FREQ_60KHZ = 3,  // default | ||||
|   PS_LED_FREQ_70KHZ = 4, | ||||
|   PS_LED_FREQ_80KHZ = 5, | ||||
|   PS_LED_FREQ_90KHZ = 6, | ||||
|   PS_LED_FREQ_100KHZ = 7, | ||||
| }; | ||||
|  | ||||
| // LED current duty | ||||
| enum PsLedDuty : uint8_t { | ||||
|   PS_LED_DUTY_25 = 0, | ||||
|   PS_LED_DUTY_50 = 1, | ||||
|   PS_LED_DUTY_75 = 2, | ||||
|   PS_LED_DUTY_100 = 3,  // default | ||||
| }; | ||||
|  | ||||
| // LED pulsed current level | ||||
| enum PsLedCurrent : uint8_t { | ||||
|   PS_LED_CURRENT_5MA = 0, | ||||
|   PS_LED_CURRENT_10MA = 1, | ||||
|   PS_LED_CURRENT_20MA = 2, | ||||
|   PS_LED_CURRENT_50MA = 3, | ||||
|   PS_LED_CURRENT_100MA = 4,  // default | ||||
|   PS_LED_CURRENT_100MA1 = 5, | ||||
|   PS_LED_CURRENT_100MA2 = 6, | ||||
|   PS_LED_CURRENT_100MA3 = 7, | ||||
| }; | ||||
|  | ||||
| // PS measurement rate | ||||
| enum PsMeasurementRate : uint8_t { | ||||
|   PS_MEAS_RATE_50MS = 0, | ||||
|   PS_MEAS_RATE_70MS = 1, | ||||
|   PS_MEAS_RATE_100MS = 2, | ||||
|   PS_MEAS_RATE_200MS = 3, | ||||
|   PS_MEAS_RATE_500MS = 4,  // default | ||||
|   PS_MEAS_RATE_1000MS = 5, | ||||
|   PS_MEAS_RATE_2000MS = 6, | ||||
|   PS_MEAS_RATE_2000MS1 = 7, | ||||
|   PS_MEAS_RATE_10MS = 8, | ||||
| }; | ||||
|  | ||||
| // | ||||
| // ALS_CONTR Register (0x80) | ||||
| // | ||||
| union AlsControlRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     bool active_mode : 1; | ||||
|     bool sw_reset : 1; | ||||
|     AlsGain gain : 3; | ||||
|     uint8_t reserved : 3; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // PS_CONTR Register (0x81) | ||||
| // | ||||
| union PsControlRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     bool ps_mode_xxx : 1; | ||||
|     bool ps_mode_active : 1; | ||||
|     PsGain ps_gain : 2;  // only LTR-659/558 | ||||
|     bool reserved_4 : 1; | ||||
|     bool ps_saturation_indicator_enable : 1; | ||||
|     bool reserved_6 : 1; | ||||
|     bool reserved_7 : 1; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // PS_LED Register (0x82) | ||||
| // | ||||
| union PsLedRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     PsLedCurrent ps_led_current : 3; | ||||
|     PsLedDuty ps_led_duty : 2; | ||||
|     PsLedFreq ps_led_freq : 3; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // PS_N_PULSES Register (0x83) | ||||
| // | ||||
| union PsNPulsesRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     uint8_t number_of_pulses : 4; | ||||
|     uint8_t reserved : 4; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // PS_MEAS_RATE Register (0x84) | ||||
| // | ||||
| union PsMeasurementRateRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     PsMeasurementRate ps_measurement_rate : 4; | ||||
|     uint8_t reserved : 4; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // ALS_MEAS_RATE Register (0x85) | ||||
| // | ||||
| union MeasurementRateRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     MeasurementRepeatRate measurement_repeat_rate : 3; | ||||
|     IntegrationTime integration_time : 3; | ||||
|     bool reserved_6 : 1; | ||||
|     bool reserved_7 : 1; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // PART_ID Register (0x86) (Read Only) | ||||
| // | ||||
| union PartIdRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     uint8_t part_number_id : 4; | ||||
|     uint8_t revision_id : 4; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // ALS_PS_STATUS Register (0x8C) (Read Only) | ||||
| // | ||||
| union AlsPsStatusRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     bool ps_new_data : 1;    // 0 - old data, 1 - new data | ||||
|     bool ps_interrupt : 1;   // 0 - interrupt signal not active, 1 - interrupt signal active | ||||
|     bool als_new_data : 1;   // 0 - old data, 1 - new data | ||||
|     bool als_interrupt : 1;  // 0 - interrupt signal not active, 1 - interrupt signal active | ||||
|     AlsGain gain : 3;        // current ALS gain | ||||
|     bool data_invalid : 1; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // PS_DATA_1 Register (0x8E) (Read Only) | ||||
| // | ||||
| union PsData1Register { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     uint8_t ps_data_high : 3; | ||||
|     uint8_t reserved : 4; | ||||
|     bool ps_saturation_flag : 1; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // INTERRUPT Register (0x8F) (Read Only) | ||||
| // | ||||
| union InterruptRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     bool ps_interrupt : 1; | ||||
|     bool als_interrupt : 1; | ||||
|     bool interrupt_polarity : 1;  // 0 - active low (default), 1 - active high | ||||
|     uint8_t reserved : 5; | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| // | ||||
| // INTERRUPT_PERSIST Register (0x9E) | ||||
| // | ||||
| union InterruptPersistRegister { | ||||
|   uint8_t raw; | ||||
|   struct { | ||||
|     uint8_t als_persist : 4;  // 0 - every ALS cycle, 1 - every 2 ALS cycles, ... 15 - every 16 ALS cycles | ||||
|     uint8_t ps_persist : 4;   // 0 - every PS cycle, 1 - every 2 PS cycles, ... 15 - every 16 PS cycles | ||||
|   } __attribute__((packed)); | ||||
| }; | ||||
|  | ||||
| }  // namespace ltr_als_ps | ||||
| }  // namespace esphome | ||||
							
								
								
									
										271
									
								
								esphome/components/ltr_als_ps/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								esphome/components/ltr_als_ps/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,271 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_ACTUAL_GAIN, | ||||
|     CONF_AMBIENT_LIGHT, | ||||
|     CONF_AUTO_MODE, | ||||
|     CONF_GAIN, | ||||
|     CONF_GLASS_ATTENUATION_FACTOR, | ||||
|     CONF_ID, | ||||
|     CONF_INTEGRATION_TIME, | ||||
|     CONF_NAME, | ||||
|     CONF_REPEAT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     ICON_BRIGHTNESS_5, | ||||
|     ICON_BRIGHTNESS_6, | ||||
|     ICON_TIMER, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_LUX, | ||||
|     UNIT_MILLISECOND, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@latonita"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time" | ||||
| CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts" | ||||
| CONF_INFRARED_COUNTS = "infrared_counts" | ||||
| CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold" | ||||
| CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold" | ||||
| CONF_PS_COOLDOWN = "ps_cooldown" | ||||
| CONF_PS_COUNTS = "ps_counts" | ||||
| CONF_PS_GAIN = "ps_gain" | ||||
| CONF_PS_HIGH_THRESHOLD = "ps_high_threshold" | ||||
| CONF_PS_LOW_THRESHOLD = "ps_low_threshold" | ||||
| ICON_BRIGHTNESS_7 = "mdi:brightness-7" | ||||
| ICON_GAIN = "mdi:multiplication" | ||||
| ICON_PROXIMITY = "mdi:hand-wave-outline" | ||||
| UNIT_COUNTS = "#" | ||||
|  | ||||
| ltr_als_ps_ns = cg.esphome_ns.namespace("ltr_als_ps") | ||||
|  | ||||
| LTRAlsPsComponent = ltr_als_ps_ns.class_( | ||||
|     "LTRAlsPsComponent", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| LtrType = ltr_als_ps_ns.enum("LtrType") | ||||
| LTR_TYPES = { | ||||
|     "ALS": LtrType.LTR_TYPE_ALS_ONLY, | ||||
|     "PS": LtrType.LTR_TYPE_PS_ONLY, | ||||
|     "ALS_PS": LtrType.LTR_TYPE_ALS_AND_PS, | ||||
| } | ||||
|  | ||||
| AlsGain = ltr_als_ps_ns.enum("AlsGain") | ||||
| ALS_GAINS = { | ||||
|     "1X": AlsGain.GAIN_1, | ||||
|     "2X": AlsGain.GAIN_2, | ||||
|     "4X": AlsGain.GAIN_4, | ||||
|     "8X": AlsGain.GAIN_8, | ||||
|     "48X": AlsGain.GAIN_48, | ||||
|     "96X": AlsGain.GAIN_96, | ||||
| } | ||||
|  | ||||
| IntegrationTime = ltr_als_ps_ns.enum("IntegrationTime") | ||||
| INTEGRATION_TIMES = { | ||||
|     50: IntegrationTime.INTEGRATION_TIME_50MS, | ||||
|     100: IntegrationTime.INTEGRATION_TIME_100MS, | ||||
|     150: IntegrationTime.INTEGRATION_TIME_150MS, | ||||
|     200: IntegrationTime.INTEGRATION_TIME_200MS, | ||||
|     250: IntegrationTime.INTEGRATION_TIME_250MS, | ||||
|     300: IntegrationTime.INTEGRATION_TIME_300MS, | ||||
|     350: IntegrationTime.INTEGRATION_TIME_350MS, | ||||
|     400: IntegrationTime.INTEGRATION_TIME_400MS, | ||||
| } | ||||
|  | ||||
| MeasurementRepeatRate = ltr_als_ps_ns.enum("MeasurementRepeatRate") | ||||
| MEASUREMENT_REPEAT_RATES = { | ||||
|     50: MeasurementRepeatRate.REPEAT_RATE_50MS, | ||||
|     100: MeasurementRepeatRate.REPEAT_RATE_100MS, | ||||
|     200: MeasurementRepeatRate.REPEAT_RATE_200MS, | ||||
|     500: MeasurementRepeatRate.REPEAT_RATE_500MS, | ||||
|     1000: MeasurementRepeatRate.REPEAT_RATE_1000MS, | ||||
|     2000: MeasurementRepeatRate.REPEAT_RATE_2000MS, | ||||
| } | ||||
|  | ||||
| PsGain = ltr_als_ps_ns.enum("PsGain") | ||||
| PS_GAINS = { | ||||
|     "16X": PsGain.PS_GAIN_16, | ||||
|     "32X": PsGain.PS_GAIN_32, | ||||
|     "64X": PsGain.PS_GAIN_64, | ||||
| } | ||||
|  | ||||
| LTRPsHighTrigger = ltr_als_ps_ns.class_( | ||||
|     "LTRPsHighTrigger", automation.Trigger.template() | ||||
| ) | ||||
| LTRPsLowTrigger = ltr_als_ps_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) | ||||
|  | ||||
|  | ||||
| def validate_integration_time(value): | ||||
|     value = cv.positive_time_period_milliseconds(value).total_milliseconds | ||||
|     return cv.enum(INTEGRATION_TIMES, int=True)(value) | ||||
|  | ||||
|  | ||||
| def validate_repeat_rate(value): | ||||
|     value = cv.positive_time_period_milliseconds(value).total_milliseconds | ||||
|     return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) | ||||
|  | ||||
|  | ||||
| def validate_time_and_repeat_rate(config): | ||||
|     integraton_time = config[CONF_INTEGRATION_TIME] | ||||
|     repeat_rate = config[CONF_REPEAT] | ||||
|     if integraton_time > repeat_rate: | ||||
|         raise cv.Invalid( | ||||
|             f"Measurement repeat rate ({repeat_rate}ms) shall be greater or equal to integration time ({integraton_time}ms)" | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(LTRAlsPsComponent), | ||||
|             cv.Optional(CONF_TYPE, default="ALS_PS"): cv.enum(LTR_TYPES, upper=True), | ||||
|             cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_GAIN, default="1X"): cv.enum(ALS_GAINS, upper=True), | ||||
|             cv.Optional( | ||||
|                 CONF_INTEGRATION_TIME, default="100ms" | ||||
|             ): validate_integration_time, | ||||
|             cv.Optional(CONF_REPEAT, default="500ms"): validate_repeat_rate, | ||||
|             cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( | ||||
|                 min=1.0 | ||||
|             ), | ||||
|             cv.Optional( | ||||
|                 CONF_PS_COOLDOWN, default="5s" | ||||
|             ): cv.positive_time_period_seconds, | ||||
|             cv.Optional(CONF_PS_GAIN, default="16X"): cv.enum(PS_GAINS, upper=True), | ||||
|             cv.Optional(CONF_PS_HIGH_THRESHOLD, default=65535): cv.int_range( | ||||
|                 min=0, max=65535 | ||||
|             ), | ||||
|             cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( | ||||
|                 min=0, max=65535 | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( | ||||
|                 sensor.sensor_schema( | ||||
|                     unit_of_measurement=UNIT_LUX, | ||||
|                     icon=ICON_BRIGHTNESS_6, | ||||
|                     accuracy_decimals=1, | ||||
|                     device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|                     state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 ), | ||||
|                 key=CONF_NAME, | ||||
|             ), | ||||
|             cv.Optional(CONF_INFRARED_COUNTS): cv.maybe_simple_value( | ||||
|                 sensor.sensor_schema( | ||||
|                     unit_of_measurement=UNIT_COUNTS, | ||||
|                     icon=ICON_BRIGHTNESS_5, | ||||
|                     accuracy_decimals=0, | ||||
|                     device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|                     state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 ), | ||||
|                 key=CONF_NAME, | ||||
|             ), | ||||
|             cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value( | ||||
|                 sensor.sensor_schema( | ||||
|                     unit_of_measurement=UNIT_COUNTS, | ||||
|                     icon=ICON_BRIGHTNESS_7, | ||||
|                     accuracy_decimals=0, | ||||
|                     device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|                     state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 ), | ||||
|                 key=CONF_NAME, | ||||
|             ), | ||||
|             cv.Optional(CONF_PS_COUNTS): cv.maybe_simple_value( | ||||
|                 sensor.sensor_schema( | ||||
|                     unit_of_measurement=UNIT_COUNTS, | ||||
|                     icon=ICON_PROXIMITY, | ||||
|                     accuracy_decimals=0, | ||||
|                     device_class=DEVICE_CLASS_DISTANCE, | ||||
|                     state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 ), | ||||
|                 key=CONF_NAME, | ||||
|             ), | ||||
|             cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value( | ||||
|                 sensor.sensor_schema( | ||||
|                     icon=ICON_GAIN, | ||||
|                     accuracy_decimals=0, | ||||
|                     device_class=DEVICE_CLASS_ILLUMINANCE, | ||||
|                     state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 ), | ||||
|                 key=CONF_NAME, | ||||
|             ), | ||||
|             cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value( | ||||
|                 sensor.sensor_schema( | ||||
|                     unit_of_measurement=UNIT_MILLISECOND, | ||||
|                     icon=ICON_TIMER, | ||||
|                     accuracy_decimals=0, | ||||
|                     state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 ), | ||||
|                 key=CONF_NAME, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x29)), | ||||
|     validate_time_and_repeat_rate, | ||||
| ) | ||||
|  | ||||
|  | ||||
| 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 als_config := config.get(CONF_AMBIENT_LIGHT): | ||||
|         sens = await sensor.new_sensor(als_config) | ||||
|         cg.add(var.set_ambient_light_sensor(sens)) | ||||
|  | ||||
|     if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS): | ||||
|         sens = await sensor.new_sensor(infrared_cnt_config) | ||||
|         cg.add(var.set_infrared_counts_sensor(sens)) | ||||
|  | ||||
|     if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS): | ||||
|         sens = await sensor.new_sensor(full_spect_cnt_config) | ||||
|         cg.add(var.set_full_spectrum_counts_sensor(sens)) | ||||
|  | ||||
|     if act_gain_config := config.get(CONF_ACTUAL_GAIN): | ||||
|         sens = await sensor.new_sensor(act_gain_config) | ||||
|         cg.add(var.set_actual_gain_sensor(sens)) | ||||
|  | ||||
|     if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME): | ||||
|         sens = await sensor.new_sensor(act_itime_config) | ||||
|         cg.add(var.set_actual_integration_time_sensor(sens)) | ||||
|  | ||||
|     if prox_cnt_config := config.get(CONF_PS_COUNTS): | ||||
|         sens = await sensor.new_sensor(prox_cnt_config) | ||||
|         cg.add(var.set_proximity_counts_sensor(sens)) | ||||
|  | ||||
|     for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): | ||||
|         trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], prox_high_tr) | ||||
|  | ||||
|     for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): | ||||
|         trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], prox_low_tr) | ||||
|  | ||||
|     cg.add(var.set_ltr_type(config[CONF_TYPE])) | ||||
|  | ||||
|     cg.add(var.set_als_auto_mode(config[CONF_AUTO_MODE])) | ||||
|     cg.add(var.set_als_gain(config[CONF_GAIN])) | ||||
|     cg.add(var.set_als_integration_time(config[CONF_INTEGRATION_TIME])) | ||||
|     cg.add(var.set_als_meas_repeat_rate(config[CONF_REPEAT])) | ||||
|     cg.add(var.set_als_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) | ||||
|  | ||||
|     cg.add(var.set_ps_cooldown_time_s(config[CONF_PS_COOLDOWN])) | ||||
|     cg.add(var.set_ps_gain(config[CONF_PS_GAIN])) | ||||
|     cg.add(var.set_ps_high_threshold(config[CONF_PS_HIGH_THRESHOLD])) | ||||
|     cg.add(var.set_ps_low_threshold(config[CONF_PS_LOW_THRESHOLD])) | ||||
		Reference in New Issue
	
	Block a user