mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 16:41:50 +00:00 
			
		
		
		
	Compare commits
	
		
			30 Commits
		
	
	
		
			2022.5.0b1
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9268ef7665 | ||
| 
						 | 
					cd35ead890 | ||
| 
						 | 
					9dc804ee27 | ||
| 
						 | 
					a8ceeaa7b0 | ||
| 
						 | 
					7092f7663e | ||
| 
						 | 
					d9d2edeb08 | ||
| 
						 | 
					dda1ddcb26 | ||
| 
						 | 
					f0c890f160 | ||
| 
						 | 
					4f52d43347 | ||
| 
						 | 
					0ed7db979b | ||
| 
						 | 
					9c78049359 | ||
| 
						 | 
					7882105661 | ||
| 
						 | 
					c000e1d6dd | ||
| 
						 | 
					9b6b9c1fa2 | ||
| 
						 | 
					609a2ca592 | ||
| 
						 | 
					6dabf24bf3 | ||
| 
						 | 
					93e2506279 | ||
| 
						 | 
					f62d5d3b9d | ||
| 
						 | 
					0665acd190 | ||
| 
						 | 
					fea05e9d33 | ||
| 
						 | 
					7a03c7d56f | ||
| 
						 | 
					2dc2aec954 | ||
| 
						 | 
					39c6c2417a | ||
| 
						 | 
					03d5a0ec1d | ||
| 
						 | 
					1c873e0034 | ||
| 
						 | 
					bcb47c306c | ||
| 
						 | 
					01c4d3c225 | ||
| 
						 | 
					c2aaae4818 | ||
| 
						 | 
					3f678e218d | ||
| 
						 | 
					f8a1bd4e79 | 
@@ -178,6 +178,7 @@ esphome/components/sen5x/* @martgras
 | 
			
		||||
esphome/components/sensirion_common/* @martgras
 | 
			
		||||
esphome/components/sensor/* @esphome/core
 | 
			
		||||
esphome/components/sgp40/* @SenexCrenshaw
 | 
			
		||||
esphome/components/sgp4x/* @SenexCrenshaw @martgras
 | 
			
		||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
 | 
			
		||||
esphome/components/sht4x/* @sjtrny
 | 
			
		||||
esphome/components/shutdown/* @esphome/core @jsuanet
 | 
			
		||||
@@ -222,6 +223,7 @@ esphome/components/tsl2591/* @wjcarpenter
 | 
			
		||||
esphome/components/tuya/binary_sensor/* @jesserockz
 | 
			
		||||
esphome/components/tuya/climate/* @jesserockz
 | 
			
		||||
esphome/components/tuya/number/* @frankiboy1
 | 
			
		||||
esphome/components/tuya/select/* @bearpawmaxim
 | 
			
		||||
esphome/components/tuya/sensor/* @jesserockz
 | 
			
		||||
esphome/components/tuya/switch/* @jesserockz
 | 
			
		||||
esphome/components/tuya/text_sensor/* @dentra
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
      // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
 | 
			
		||||
      // also take into account min_power
 | 
			
		||||
      auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
      this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
 | 
			
		||||
      // calculate required value to provide a true RMS voltage output
 | 
			
		||||
      this->enable_time_us =
 | 
			
		||||
          std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) *
 | 
			
		||||
                                            (this->cycle_time_us - min_us)) /
 | 
			
		||||
                                     65535);
 | 
			
		||||
      if (this->method == DIM_METHOD_LEADING_PULSE) {
 | 
			
		||||
        // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
 | 
			
		||||
        // this is for brightness near 99%
 | 
			
		||||
 
 | 
			
		||||
@@ -76,8 +76,6 @@ async def to_code(config):
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                image.thumbnail(config[CONF_RESIZE])
 | 
			
		||||
            frame = image.convert("RGB")
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,6 @@
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
#include "esphome/components/homeassistant/time/homeassistant_time.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
bool APIConnection::send_fan_state(fan::Fan *fan) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
 | 
			
		||||
    resp.oscillating = fan->oscillating;
 | 
			
		||||
  if (traits.supports_speed()) {
 | 
			
		||||
    resp.speed_level = fan->speed;
 | 
			
		||||
    resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count()));
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_direction())
 | 
			
		||||
    resp.direction = static_cast<enums::FanDirection>(fan->direction);
 | 
			
		||||
@@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
  if (fan == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto traits = fan->get_traits();
 | 
			
		||||
 | 
			
		||||
  auto call = fan->make_call();
 | 
			
		||||
  if (msg.has_state)
 | 
			
		||||
    call.set_state(msg.state);
 | 
			
		||||
@@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
  if (msg.has_speed_level) {
 | 
			
		||||
    // Prefer level
 | 
			
		||||
    call.set_speed(msg.speed_level);
 | 
			
		||||
  } else if (msg.has_speed) {
 | 
			
		||||
    call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count()));
 | 
			
		||||
  }
 | 
			
		||||
  if (msg.has_direction)
 | 
			
		||||
    call.set_direction(static_cast<fan::FanDirection>(msg.direction));
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@ void Bedjet::control(const ClimateCall &call) {
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_OFF);
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_EXTHT);
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_HEAT);
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_COOL);
 | 
			
		||||
@@ -137,7 +137,7 @@ void Bedjet::control(const ClimateCall &call) {
 | 
			
		||||
    } else {
 | 
			
		||||
      this->force_refresh_ = true;
 | 
			
		||||
      this->mode = mode;
 | 
			
		||||
      // We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those
 | 
			
		||||
      // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
    }
 | 
			
		||||
@@ -186,6 +186,8 @@ void Bedjet::control(const ClimateCall &call) {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_M2);
 | 
			
		||||
    } else if (preset == "M3") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_M3);
 | 
			
		||||
    } else if (preset == "EXT HT") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_EXTHT);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "bedjet_base.h"
 | 
			
		||||
 | 
			
		||||
@@ -67,6 +68,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
 | 
			
		||||
        // We could fetch biodata from bedjet and set these names that way.
 | 
			
		||||
        // But then we have to invert the lookup in order to send the right preset.
 | 
			
		||||
        // For now, we can leave them as M1-3 to match the remote buttons.
 | 
			
		||||
        // EXT HT added to match remote button.
 | 
			
		||||
        "EXT HT",
 | 
			
		||||
        "M1",
 | 
			
		||||
        "M2",
 | 
			
		||||
        "M3",
 | 
			
		||||
 
 | 
			
		||||
@@ -66,8 +66,8 @@ enum BedjetCommand : uint8_t {
 | 
			
		||||
 | 
			
		||||
#define BEDJET_FAN_STEP_NAMES_ \
 | 
			
		||||
  { \
 | 
			
		||||
    "  5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \
 | 
			
		||||
        " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \
 | 
			
		||||
    "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \
 | 
			
		||||
        "85%", "90%", "95%", "100%" \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
 
 | 
			
		||||
@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
    if (request->method() == HTTP_GET) {
 | 
			
		||||
      if (request->url() == "/")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/stylesheet.css")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-1.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-2.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-3.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-4.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/lock.svg")
 | 
			
		||||
      if (request->url() == "/config.json")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifisave")
 | 
			
		||||
        return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            validate_climate_fan_mode
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            cv.string_strict
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict),
 | 
			
		||||
        cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
 | 
			
		||||
        cg.add(var.set_fan_mode(template_))
 | 
			
		||||
    if CONF_CUSTOM_FAN_MODE in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
            config[CONF_CUSTOM_FAN_MODE], args, cg.std_string
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_custom_fan_mode(template_))
 | 
			
		||||
    if CONF_PRESET in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
 | 
			
		||||
        cg.add(var.set_preset(template_))
 | 
			
		||||
    if CONF_CUSTOM_PRESET in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
            config[CONF_CUSTOM_PRESET], args, cg.std_string
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_custom_preset(template_))
 | 
			
		||||
    if CONF_SWING_MODE in config:
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,9 @@ class ColorUtil {
 | 
			
		||||
    }
 | 
			
		||||
    return color_return;
 | 
			
		||||
  }
 | 
			
		||||
  static inline Color rgb332_to_color(uint8_t rgb332_color) {
 | 
			
		||||
    return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332);
 | 
			
		||||
  }
 | 
			
		||||
  static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
 | 
			
		||||
    uint16_t red_color, green_color, blue_color;
 | 
			
		||||
 | 
			
		||||
@@ -100,11 +103,57 @@ class ColorUtil {
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static uint32_t color_to_grayscale4(Color color) {
 | 
			
		||||
    uint32_t gs4 = esp_scale8(color.white, 15);
 | 
			
		||||
    return gs4;
 | 
			
		||||
  }
 | 
			
		||||
  /***
 | 
			
		||||
   * Converts a Color value to an 8bit index using a 24bit 888 palette.
 | 
			
		||||
   * Uses euclidiean distance to calculate the linear distance between
 | 
			
		||||
   * two points in an RGB cube, then iterates through the full palette
 | 
			
		||||
   * returning the closest match.
 | 
			
		||||
   * @param[in] color The target color.
 | 
			
		||||
   * @param[in] palette The 256*3 byte RGB palette.
 | 
			
		||||
   * @return The 8 bit index of the closest color (e.g. for display buffer).
 | 
			
		||||
   */
 | 
			
		||||
  // static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) {
 | 
			
		||||
  static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) {
 | 
			
		||||
    uint8_t closest_index = 0;
 | 
			
		||||
    uint32_t minimum_dist2 = UINT32_MAX;  // Smallest distance^2 to the target
 | 
			
		||||
                                          // so far
 | 
			
		||||
    // int8_t(*plt)[][3] = palette;
 | 
			
		||||
    int16_t tgt_r = color.r;
 | 
			
		||||
    int16_t tgt_g = color.g;
 | 
			
		||||
    int16_t tgt_b = color.b;
 | 
			
		||||
    uint16_t x, y, z;
 | 
			
		||||
    // Loop through each row of the palette
 | 
			
		||||
    for (uint16_t i = 0; i < 256; i++) {
 | 
			
		||||
      // Get the pallet rgb color
 | 
			
		||||
      int16_t plt_r = (int16_t) palette[i * 3 + 0];
 | 
			
		||||
      int16_t plt_g = (int16_t) palette[i * 3 + 1];
 | 
			
		||||
      int16_t plt_b = (int16_t) palette[i * 3 + 2];
 | 
			
		||||
      // Calculate euclidian distance (linear distance in rgb cube).
 | 
			
		||||
      x = (uint32_t) std::abs(tgt_r - plt_r);
 | 
			
		||||
      y = (uint32_t) std::abs(tgt_g - plt_g);
 | 
			
		||||
      z = (uint32_t) std::abs(tgt_b - plt_b);
 | 
			
		||||
      uint32_t dist2 = x * x + y * y + z * z;
 | 
			
		||||
      if (dist2 < minimum_dist2) {
 | 
			
		||||
        minimum_dist2 = dist2;
 | 
			
		||||
        closest_index = (uint8_t) i;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return closest_index;
 | 
			
		||||
  }
 | 
			
		||||
  /***
 | 
			
		||||
   * Converts an 8bit palette index (e.g. from a display buffer) to a color.
 | 
			
		||||
   * @param[in] index The index to look up.
 | 
			
		||||
   * @param[in] palette The 256*3 byte RGB palette.
 | 
			
		||||
   * @return The RGBW Color object looked up by the palette.
 | 
			
		||||
   */
 | 
			
		||||
  static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) {
 | 
			
		||||
    Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0);
 | 
			
		||||
    return color;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
    // go through vector from back to front (makes erase easier/more efficient)
 | 
			
		||||
    for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
 | 
			
		||||
      const auto &save = s_pending_save[i];
 | 
			
		||||
      esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
			
		||||
                 esp_err_to_name(err));
 | 
			
		||||
        any_failed = true;
 | 
			
		||||
        continue;
 | 
			
		||||
      ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
 | 
			
		||||
      if (is_changed(nvs_handle, save)) {
 | 
			
		||||
        esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
 | 
			
		||||
        if (err != 0) {
 | 
			
		||||
          ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
			
		||||
                   esp_err_to_name(err));
 | 
			
		||||
          any_failed = true;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "NVS data not changed skipping %s  len=%u", save.key.c_str(), save.data.size());
 | 
			
		||||
      }
 | 
			
		||||
      s_pending_save.erase(s_pending_save.begin() + i);
 | 
			
		||||
    }
 | 
			
		||||
@@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
 | 
			
		||||
    return !any_failed;
 | 
			
		||||
  }
 | 
			
		||||
  bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
 | 
			
		||||
    NVSData stored_data{};
 | 
			
		||||
    size_t actual_len;
 | 
			
		||||
    esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
 | 
			
		||||
    if (err != 0) {
 | 
			
		||||
      ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    stored_data.data.reserve(actual_len);
 | 
			
		||||
    err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len);
 | 
			
		||||
    if (err != 0) {
 | 
			
		||||
      ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    return to_save.data != stored_data.data;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void setup_preferences() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
@@ -12,6 +13,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_RESOLUTION,
 | 
			
		||||
    CONF_BRIGHTNESS,
 | 
			
		||||
    CONF_CONTRAST,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
@@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"]
 | 
			
		||||
 | 
			
		||||
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
 | 
			
		||||
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
 | 
			
		||||
 | 
			
		||||
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
 | 
			
		||||
    "ESP32CameraStreamStartTrigger",
 | 
			
		||||
    automation.Trigger.template(),
 | 
			
		||||
)
 | 
			
		||||
ESP32CameraStreamStopTrigger = esp32_camera_ns.class_(
 | 
			
		||||
    "ESP32CameraStreamStopTrigger",
 | 
			
		||||
    automation.Trigger.template(),
 | 
			
		||||
)
 | 
			
		||||
ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
 | 
			
		||||
FRAME_SIZES = {
 | 
			
		||||
    "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
 | 
			
		||||
@@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern"
 | 
			
		||||
CONF_MAX_FRAMERATE = "max_framerate"
 | 
			
		||||
CONF_IDLE_FRAMERATE = "idle_framerate"
 | 
			
		||||
 | 
			
		||||
# stream trigger
 | 
			
		||||
CONF_ON_STREAM_START = "on_stream_start"
 | 
			
		||||
CONF_ON_STREAM_STOP = "on_stream_stop"
 | 
			
		||||
 | 
			
		||||
camera_range_param = cv.int_range(min=-2, max=2)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
@@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
        cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
 | 
			
		||||
            cv.framerate, cv.Range(min=0, max=1)
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32CameraStreamStartTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32CameraStreamStopTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -238,3 +265,11 @@ async def to_code(config):
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        cg.add_library("espressif/esp32-camera", "1.0.0")
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_STREAM_START, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_STREAM_STOP, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 
 | 
			
		||||
@@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
 | 
			
		||||
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
 | 
			
		||||
  this->new_image_callback_.add(std::move(f));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); }
 | 
			
		||||
void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); }
 | 
			
		||||
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->stream_start_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::add_stream_stop_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->stream_stop_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::start_stream(CameraRequester requester) {
 | 
			
		||||
  this->stream_start_callback_.call();
 | 
			
		||||
  this->stream_requesters_ |= (1U << requester);
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::stop_stream(CameraRequester requester) {
 | 
			
		||||
  this->stream_stop_callback_.call();
 | 
			
		||||
  this->stream_requesters_ &= ~(1U << requester);
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
 | 
			
		||||
void ESP32Camera::update_camera_parameters() {
 | 
			
		||||
  sensor_t *s = esp_camera_sensor_get();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
@@ -145,6 +146,9 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
  void request_image(CameraRequester requester);
 | 
			
		||||
  void update_camera_parameters();
 | 
			
		||||
 | 
			
		||||
  void add_stream_start_callback(std::function<void()> &&callback);
 | 
			
		||||
  void add_stream_stop_callback(std::function<void()> &&callback);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /* internal methods */
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
@@ -187,6 +191,8 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
  QueueHandle_t framebuffer_get_queue_;
 | 
			
		||||
  QueueHandle_t framebuffer_return_queue_;
 | 
			
		||||
  CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_;
 | 
			
		||||
  CallbackManager<void()> stream_start_callback_{};
 | 
			
		||||
  CallbackManager<void()> stream_stop_callback_{};
 | 
			
		||||
 | 
			
		||||
  uint32_t last_idle_request_{0};
 | 
			
		||||
  uint32_t last_update_{0};
 | 
			
		||||
@@ -195,6 +201,23 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
extern ESP32Camera *global_esp32_camera;
 | 
			
		||||
 | 
			
		||||
class ESP32CameraStreamStartTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {
 | 
			
		||||
    parent->add_stream_start_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
class ESP32CameraStreamStopTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) {
 | 
			
		||||
    parent->add_stream_stop_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_camera
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
#include "fan_helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -61,22 +60,6 @@ void FanCall::validate_() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
FanCall &FanCall::set_speed(const char *legacy_speed) {
 | 
			
		||||
  const auto supported_speed_count = this->parent_.get_traits().supported_speed_count();
 | 
			
		||||
  if (strcasecmp(legacy_speed, "low") == 0) {
 | 
			
		||||
    this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count));
 | 
			
		||||
  } else if (strcasecmp(legacy_speed, "medium") == 0) {
 | 
			
		||||
    this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count));
 | 
			
		||||
  } else if (strcasecmp(legacy_speed, "high") == 0) {
 | 
			
		||||
    this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count));
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
 | 
			
		||||
FanCall FanRestoreState::to_call(Fan &fan) {
 | 
			
		||||
  auto call = fan.make_call();
 | 
			
		||||
  call.set_state(this->state);
 | 
			
		||||
 
 | 
			
		||||
@@ -16,13 +16,6 @@ namespace fan {
 | 
			
		||||
    (obj)->dump_traits_(TAG, prefix); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
 | 
			
		||||
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
 | 
			
		||||
  FAN_SPEED_LOW = 0,     ///< The fan is running on low speed.
 | 
			
		||||
  FAN_SPEED_MEDIUM = 1,  ///< The fan is running on medium speed.
 | 
			
		||||
  FAN_SPEED_HIGH = 2     ///< The fan is running on high/full speed.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Simple enum to represent the direction of a fan.
 | 
			
		||||
enum class FanDirection { FORWARD = 0, REVERSE = 1 };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#include <cassert>
 | 
			
		||||
#include "fan_helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
// This whole file is deprecated, don't warn about usage of deprecated types in here.
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
 | 
			
		||||
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
 | 
			
		||||
  const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
 | 
			
		||||
  const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);
 | 
			
		||||
  return static_cast<FanSpeed>(legacy_level - 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) {
 | 
			
		||||
  const auto enum_level = static_cast<int>(speed) + 1;
 | 
			
		||||
  const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels);
 | 
			
		||||
  return static_cast<int>(speed_level);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
// Shut-up about usage of deprecated FanSpeed for a bit.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
 | 
			
		||||
ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
 | 
			
		||||
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
 | 
			
		||||
ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
 | 
			
		||||
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "hbridge_fan.h"
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,16 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.components import display, spi
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_COLOR_PALETTE,
 | 
			
		||||
    CONF_DC_PIN,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_MODEL,
 | 
			
		||||
    CONF_PAGES,
 | 
			
		||||
    CONF_RAW_DATA_ID,
 | 
			
		||||
    CONF_RESET_PIN,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import HexInt
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +26,7 @@ ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341)
 | 
			
		||||
ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341)
 | 
			
		||||
 | 
			
		||||
ILI9341Model = ili9341_ns.enum("ILI9341Model")
 | 
			
		||||
ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
 | 
			
		||||
 | 
			
		||||
MODELS = {
 | 
			
		||||
    "M5STACK": ILI9341Model.M5STACK,
 | 
			
		||||
@@ -31,6 +35,8 @@ MODELS = {
 | 
			
		||||
 | 
			
		||||
ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
 | 
			
		||||
 | 
			
		||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE")
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    display.FULL_DISPLAY_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
@@ -39,6 +45,8 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE,
 | 
			
		||||
            cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("1s"))
 | 
			
		||||
@@ -73,3 +81,13 @@ async def to_code(config):
 | 
			
		||||
    if CONF_LED_PIN in config:
 | 
			
		||||
        led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
 | 
			
		||||
        cg.add(var.set_led_pin(led_pin))
 | 
			
		||||
 | 
			
		||||
    if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
 | 
			
		||||
        cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
 | 
			
		||||
        rhs = []
 | 
			
		||||
        for x in range(256):
 | 
			
		||||
            rhs.extend([HexInt(x), HexInt(x), HexInt(x)])
 | 
			
		||||
        prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 | 
			
		||||
        cg.add(var.set_palette(prog_arr))
 | 
			
		||||
    else:
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -112,29 +112,9 @@ void ILI9341Display::display_() {
 | 
			
		||||
  this->y_high_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) {
 | 
			
		||||
  int r = color_8bit >> 5;
 | 
			
		||||
  int g = (color_8bit >> 2) & 0x07;
 | 
			
		||||
  int b = color_8bit & 0x03;
 | 
			
		||||
  uint16_t color = (r * 0x04) << 11;
 | 
			
		||||
  color |= (g * 0x09) << 5;
 | 
			
		||||
  color |= (b * 0x0A);
 | 
			
		||||
 | 
			
		||||
  return color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) {
 | 
			
		||||
  // convert 16bit color to 8 bit buffer
 | 
			
		||||
  uint8_t r = color_16bit >> 11;
 | 
			
		||||
  uint8_t g = (color_16bit >> 5) & 0x3F;
 | 
			
		||||
  uint8_t b = color_16bit & 0x1F;
 | 
			
		||||
 | 
			
		||||
  return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ILI9341Display::fill(Color color) {
 | 
			
		||||
  auto color565 = display::ColorUtil::color_to_565(color);
 | 
			
		||||
  memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_());
 | 
			
		||||
  uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
 | 
			
		||||
  memset(this->buffer_, color332, this->get_buffer_length_());
 | 
			
		||||
  this->x_low_ = 0;
 | 
			
		||||
  this->y_low_ = 0;
 | 
			
		||||
  this->x_high_ = this->get_width_internal() - 1;
 | 
			
		||||
@@ -181,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color)
 | 
			
		||||
  this->y_high_ = (y > this->y_high_) ? y : this->y_high_;
 | 
			
		||||
 | 
			
		||||
  uint32_t pos = (y * width_) + x;
 | 
			
		||||
  auto color565 = display::ColorUtil::color_to_565(color);
 | 
			
		||||
  buffer_[pos] = convert_to_8bit_color_(color565);
 | 
			
		||||
  if (this->buffer_color_mode_ == BITS_8) {
 | 
			
		||||
    uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
 | 
			
		||||
    buffer_[pos] = color332;
 | 
			
		||||
  } else {  // if (this->buffer_color_mode_ == BITS_8_INDEXED) {
 | 
			
		||||
    uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_);
 | 
			
		||||
    buffer_[pos] = index;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
 | 
			
		||||
@@ -247,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint32_t i = 0; i < sz; ++i) {
 | 
			
		||||
    uint16_t color = convert_to_16bit_color_(*src++);
 | 
			
		||||
    uint16_t color;
 | 
			
		||||
    if (this->buffer_color_mode_ == BITS_8) {
 | 
			
		||||
      color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++));
 | 
			
		||||
    } else {  //  if (this->buffer_color_mode == BITS_8_INDEXED) {
 | 
			
		||||
      Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_);
 | 
			
		||||
      color = display::ColorUtil::color_to_565(col);
 | 
			
		||||
    }
 | 
			
		||||
    *dst++ = (uint8_t)(color >> 8);
 | 
			
		||||
    *dst++ = (uint8_t) color;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,11 @@ enum ILI9341Model {
 | 
			
		||||
  TFT_24,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum ILI9341ColorMode {
 | 
			
		||||
  BITS_8,
 | 
			
		||||
  BITS_8_INDEXED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ILI9341Display : public PollingComponent,
 | 
			
		||||
                       public display::DisplayBuffer,
 | 
			
		||||
                       public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
 | 
			
		||||
@@ -24,6 +29,8 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
  void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
 | 
			
		||||
  void set_led_pin(GPIOPin *led) { this->led_pin_ = led; }
 | 
			
		||||
  void set_model(ILI9341Model model) { this->model_ = model; }
 | 
			
		||||
  void set_palette(const uint8_t *palette) { this->palette_ = palette; }
 | 
			
		||||
  void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; }
 | 
			
		||||
 | 
			
		||||
  void command(uint8_t value);
 | 
			
		||||
  void data(uint8_t value);
 | 
			
		||||
@@ -51,8 +58,6 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
  void reset_();
 | 
			
		||||
  void fill_internal_(Color color);
 | 
			
		||||
  void display_();
 | 
			
		||||
  uint16_t convert_to_16bit_color_(uint8_t color_8bit);
 | 
			
		||||
  uint8_t convert_to_8bit_color_(uint16_t color_16bit);
 | 
			
		||||
 | 
			
		||||
  ILI9341Model model_;
 | 
			
		||||
  int16_t width_{320};   ///< Display width as modified by current rotation
 | 
			
		||||
@@ -61,6 +66,9 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
  uint16_t y_low_{0};
 | 
			
		||||
  uint16_t x_high_{0};
 | 
			
		||||
  uint16_t y_high_{0};
 | 
			
		||||
  const uint8_t *palette_;
 | 
			
		||||
 | 
			
		||||
  ILI9341ColorMode buffer_color_mode_{BITS_8};
 | 
			
		||||
 | 
			
		||||
  uint32_t get_buffer_length_();
 | 
			
		||||
  int get_width_internal() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER
 | 
			
		||||
from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG
 | 
			
		||||
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
@@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_logger_baud_rate(config):
 | 
			
		||||
def validate_logger(config):
 | 
			
		||||
    logger_conf = fv.full_config.get()[CONF_LOGGER]
 | 
			
		||||
    if logger_conf[CONF_BAUD_RATE] == 0:
 | 
			
		||||
        raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "improv_serial does not support the selected logger hardware_uart"
 | 
			
		||||
            )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = validate_logger
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) {
 | 
			
		||||
  const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  const size_t request_size = std::min(free_heap, (size_t) 512);
 | 
			
		||||
 | 
			
		||||
  DynamicJsonDocument json_document(request_size);
 | 
			
		||||
  if (json_document.capacity() == 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
 | 
			
		||||
             request_size, free_heap);
 | 
			
		||||
    return "{}";
 | 
			
		||||
  size_t request_size = std::min(free_heap, (size_t) 512);
 | 
			
		||||
  while (true) {
 | 
			
		||||
    ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
 | 
			
		||||
    DynamicJsonDocument json_document(request_size);
 | 
			
		||||
    if (json_document.capacity() == 0) {
 | 
			
		||||
      ESP_LOGE(TAG,
 | 
			
		||||
               "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
 | 
			
		||||
               request_size, free_heap);
 | 
			
		||||
      return "{}";
 | 
			
		||||
    }
 | 
			
		||||
    JsonObject root = json_document.to<JsonObject>();
 | 
			
		||||
    f(root);
 | 
			
		||||
    if (json_document.overflowed()) {
 | 
			
		||||
      if (request_size == free_heap) {
 | 
			
		||||
        ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
 | 
			
		||||
                 free_heap);
 | 
			
		||||
        return "{}";
 | 
			
		||||
      }
 | 
			
		||||
      request_size = std::min(request_size * 2, free_heap);
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    json_document.shrinkToFit();
 | 
			
		||||
    ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
 | 
			
		||||
    std::string output;
 | 
			
		||||
    serializeJson(json_document, output);
 | 
			
		||||
    return output;
 | 
			
		||||
  }
 | 
			
		||||
  JsonObject root = json_document.to<JsonObject>();
 | 
			
		||||
  f(root);
 | 
			
		||||
  json_document.shrinkToFit();
 | 
			
		||||
  ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
 | 
			
		||||
  std::string output;
 | 
			
		||||
  serializeJson(json_document, output);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void parse_json(const std::string &data, const json_parse_t &f) {
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,11 @@ template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_power_off(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PowerToggleAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_power_toggle(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ac
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
 | 
			
		||||
  void do_beeper_off() { this->set_beeper_feedback(false); }
 | 
			
		||||
  void do_power_on() { this->base_.setPowerState(true); }
 | 
			
		||||
  void do_power_off() { this->base_.setPowerState(false); }
 | 
			
		||||
  void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
 | 
			
		||||
  void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; }
 | 
			
		||||
  void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; }
 | 
			
		||||
  void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; }
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
 | 
			
		||||
            cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
 | 
			
		||||
            cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
 | 
			
		||||
            cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
 | 
			
		||||
            cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id(
 | 
			
		||||
                remote_transmitter.RemoteTransmitterComponent
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_BEEPER, default=False): cv.boolean,
 | 
			
		||||
@@ -163,6 +163,7 @@ BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action)
 | 
			
		||||
BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action)
 | 
			
		||||
PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action)
 | 
			
		||||
PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action)
 | 
			
		||||
PowerToggleAction = midea_ac_ns.class_("PowerToggleAction", automation.Action)
 | 
			
		||||
 | 
			
		||||
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -249,6 +250,16 @@ async def power_off_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Power Toggle action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "power_toggle",
 | 
			
		||||
    PowerToggleAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def power_inv_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -68,33 +68,54 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
 | 
			
		||||
  uint8_t data_len = raw[2];
 | 
			
		||||
  uint8_t data_offset = 3;
 | 
			
		||||
  // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
 | 
			
		||||
  if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
 | 
			
		||||
    data_offset = 2;
 | 
			
		||||
    data_len = 4;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Error ( msb indicates error )
 | 
			
		||||
  // response format:  Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
 | 
			
		||||
  if ((function_code & 0x80) == 0x80) {
 | 
			
		||||
    data_offset = 2;
 | 
			
		||||
    data_len = 1;
 | 
			
		||||
  }
 | 
			
		||||
  // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
 | 
			
		||||
  if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) {
 | 
			
		||||
    // Handle user-defined function, since we don't know how big this ought to be,
 | 
			
		||||
    // ideally we should delegate the entire length detection to whatever handler is
 | 
			
		||||
    // installed, but wait, there is the CRC, and if we get a hit there is a good
 | 
			
		||||
    // chance that this is a complete message ... admittedly there is a small chance is
 | 
			
		||||
    // isn't but that is quite small given the purpose of the CRC in the first place
 | 
			
		||||
    data_len = at;
 | 
			
		||||
    data_offset = 1;
 | 
			
		||||
 | 
			
		||||
  // Byte data_offset..data_offset+data_len-1: Data
 | 
			
		||||
  if (at < data_offset + data_len)
 | 
			
		||||
    return true;
 | 
			
		||||
    uint16_t computed_crc = crc16(raw, data_offset + data_len);
 | 
			
		||||
    uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
 | 
			
		||||
 | 
			
		||||
  // Byte 3+data_len: CRC_LO (over all bytes)
 | 
			
		||||
  if (at == data_offset + data_len)
 | 
			
		||||
    return true;
 | 
			
		||||
    if (computed_crc != remote_crc)
 | 
			
		||||
      return true;
 | 
			
		||||
 | 
			
		||||
  // Byte data_offset+len+1: CRC_HI (over all bytes)
 | 
			
		||||
  uint16_t computed_crc = crc16(raw, data_offset + data_len);
 | 
			
		||||
  uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
 | 
			
		||||
  if (computed_crc != remote_crc) {
 | 
			
		||||
    ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
 | 
			
		||||
    return false;
 | 
			
		||||
    ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
 | 
			
		||||
 | 
			
		||||
  } else {
 | 
			
		||||
    // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
 | 
			
		||||
    if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
 | 
			
		||||
      data_offset = 2;
 | 
			
		||||
      data_len = 4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Error ( msb indicates error )
 | 
			
		||||
    // response format:  Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
 | 
			
		||||
    if ((function_code & 0x80) == 0x80) {
 | 
			
		||||
      data_offset = 2;
 | 
			
		||||
      data_len = 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Byte data_offset..data_offset+data_len-1: Data
 | 
			
		||||
    if (at < data_offset + data_len)
 | 
			
		||||
      return true;
 | 
			
		||||
 | 
			
		||||
    // Byte 3+data_len: CRC_LO (over all bytes)
 | 
			
		||||
    if (at == data_offset + data_len)
 | 
			
		||||
      return true;
 | 
			
		||||
 | 
			
		||||
    // Byte data_offset+len+1: CRC_HI (over all bytes)
 | 
			
		||||
    uint16_t computed_crc = crc16(raw, data_offset + data_len);
 | 
			
		||||
    uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
 | 
			
		||||
    if (computed_crc != remote_crc) {
 | 
			
		||||
      ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
 | 
			
		||||
  bool found = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -51,10 +51,9 @@ void MQTTCoverComponent::setup() {
 | 
			
		||||
void MQTTCoverComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
 | 
			
		||||
  auto traits = this->cover_->get_traits();
 | 
			
		||||
  // no state topic for position
 | 
			
		||||
  bool state_topic = !traits.get_supports_position();
 | 
			
		||||
  LOG_MQTT_COMPONENT(state_topic, true)
 | 
			
		||||
  if (!state_topic) {
 | 
			
		||||
  bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
 | 
			
		||||
  LOG_MQTT_COMPONENT(true, has_command_topic)
 | 
			
		||||
  if (traits.get_supports_position()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Position State Topic: '%s'", this->get_position_state_topic().c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Position Command Topic: '%s'", this->get_position_command_topic().c_str());
 | 
			
		||||
  }
 | 
			
		||||
@@ -72,7 +71,6 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
 | 
			
		||||
    root[MQTT_OPTIMISTIC] = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_position()) {
 | 
			
		||||
    config.state_topic = false;
 | 
			
		||||
    root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
 | 
			
		||||
    root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
 | 
			
		||||
  }
 | 
			
		||||
@@ -92,17 +90,7 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
 | 
			
		||||
bool MQTTCoverComponent::publish_state() {
 | 
			
		||||
  auto traits = this->cover_->get_traits();
 | 
			
		||||
  bool success = true;
 | 
			
		||||
  if (!traits.get_supports_position()) {
 | 
			
		||||
    const char *state_s = "unknown";
 | 
			
		||||
    if (this->cover_->position == COVER_OPEN) {
 | 
			
		||||
      state_s = "open";
 | 
			
		||||
    } else if (this->cover_->position == COVER_CLOSED) {
 | 
			
		||||
      state_s = "closed";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this->publish(this->get_state_topic_(), state_s))
 | 
			
		||||
      success = false;
 | 
			
		||||
  } else {
 | 
			
		||||
  if (traits.get_supports_position()) {
 | 
			
		||||
    std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0);
 | 
			
		||||
    if (!this->publish(this->get_position_state_topic(), pos))
 | 
			
		||||
      success = false;
 | 
			
		||||
@@ -112,6 +100,14 @@ bool MQTTCoverComponent::publish_state() {
 | 
			
		||||
    if (!this->publish(this->get_tilt_state_topic(), pos))
 | 
			
		||||
      success = false;
 | 
			
		||||
  }
 | 
			
		||||
  const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING   ? "opening"
 | 
			
		||||
                        : this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing"
 | 
			
		||||
                        : this->cover_->position == COVER_CLOSED                     ? "closed"
 | 
			
		||||
                        : this->cover_->position == COVER_OPEN                       ? "open"
 | 
			
		||||
                        : traits.get_supports_position()                             ? "open"
 | 
			
		||||
                                                                                     : "unknown";
 | 
			
		||||
  if (!this->publish(this->get_state_topic_(), state_s))
 | 
			
		||||
    success = false;
 | 
			
		||||
  return success;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_MQTT
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace mqtt {
 | 
			
		||||
@@ -88,17 +87,6 @@ void MQTTFanComponent::setup() {
 | 
			
		||||
                    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_->get_traits().supports_speed()) {
 | 
			
		||||
    this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) {
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
      this->state_->make_call()
 | 
			
		||||
          .set_speed(payload.c_str())  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
          .perform();
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto f = std::bind(&MQTTFanComponent::publish_state, this);
 | 
			
		||||
  this->state_->add_on_state_callback([this, f]() { this->defer("send", f); });
 | 
			
		||||
}
 | 
			
		||||
@@ -113,8 +101,6 @@ void MQTTFanComponent::dump_config() {
 | 
			
		||||
  if (this->state_->get_traits().supports_speed()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Speed State Topic: '%s'", this->get_speed_state_topic().c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Speed Command Topic: '%s'", this->get_speed_command_topic().c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -126,10 +112,8 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
 | 
			
		||||
    root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_->get_traits().supports_speed()) {
 | 
			
		||||
    root["speed_level_command_topic"] = this->get_speed_level_command_topic();
 | 
			
		||||
    root["speed_level_state_topic"] = this->get_speed_level_state_topic();
 | 
			
		||||
    root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic();
 | 
			
		||||
    root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic();
 | 
			
		||||
    root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic();
 | 
			
		||||
    root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool MQTTFanComponent::publish_state() {
 | 
			
		||||
@@ -148,31 +132,6 @@ bool MQTTFanComponent::publish_state() {
 | 
			
		||||
    bool success = this->publish(this->get_speed_level_state_topic(), payload);
 | 
			
		||||
    failed = failed || !success;
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_speed()) {
 | 
			
		||||
    const char *payload;
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
    // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
    switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) {
 | 
			
		||||
      case FAN_SPEED_LOW: {  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
        payload = "low";
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case FAN_SPEED_MEDIUM: {  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
        payload = "medium";
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
      case FAN_SPEED_HIGH: {  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
        payload = "high";
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
    bool success = this->publish(this->get_speed_state_topic(), payload);
 | 
			
		||||
    failed = failed || !success;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return !failed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -345,6 +345,8 @@ float PN532::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
void PN532::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "PN532:");
 | 
			
		||||
  if (this->is_failed())
 | 
			
		||||
    ESP_LOGE(TAG, "Component marked as failed. Check setup logs.");
 | 
			
		||||
  switch (this->error_code_) {
 | 
			
		||||
    case NONE:
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    if (this->value_.has_value()) {
 | 
			
		||||
      this->parent_->perform_forced_calibration(value_.value());
 | 
			
		||||
      this->parent_->perform_forced_calibration(this->value_.value(x...));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,628 +0,0 @@
 | 
			
		||||
 | 
			
		||||
#include "sensirion_voc_algorithm.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sgp40 {
 | 
			
		||||
 | 
			
		||||
/* The VOC code were originally created by
 | 
			
		||||
 *  https://github.com/Sensirion/embedded-sgp
 | 
			
		||||
 * The fixed point arithmetic parts of this code were originally created by
 | 
			
		||||
 * https://github.com/PetteriAimonen/libfixmath
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/*!< the maximum value of fix16_t */
 | 
			
		||||
#define FIX16_MAXIMUM 0x7FFFFFFF
 | 
			
		||||
/*!< the minimum value of fix16_t */
 | 
			
		||||
static const uint32_t FIX16_MINIMUM = 0x80000000;
 | 
			
		||||
/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not
 | 
			
		||||
 * specified */
 | 
			
		||||
static const uint32_t FIX16_OVERFLOW = 0x80000000;
 | 
			
		||||
/*!< fix16_t value of 1 */
 | 
			
		||||
const uint32_t FIX16_ONE = 0x00010000;
 | 
			
		||||
 | 
			
		||||
inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; }
 | 
			
		||||
 | 
			
		||||
inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); }
 | 
			
		||||
 | 
			
		||||
/*! Multiplies the two given fix16_t's and returns the result. */
 | 
			
		||||
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1);
 | 
			
		||||
 | 
			
		||||
/*! Divides the first given fix16_t by the second and returns the result. */
 | 
			
		||||
static fix16_t fix16_div(fix16_t a, fix16_t b);
 | 
			
		||||
 | 
			
		||||
/*! Returns the square root of the given fix16_t. */
 | 
			
		||||
static fix16_t fix16_sqrt(fix16_t in_value);
 | 
			
		||||
 | 
			
		||||
/*! Returns the exponent (e^) of the given fix16_t. */
 | 
			
		||||
static fix16_t fix16_exp(fix16_t in_value);
 | 
			
		||||
 | 
			
		||||
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) {
 | 
			
		||||
  // Each argument is divided to 16-bit parts.
 | 
			
		||||
  //                    AB
 | 
			
		||||
  //            *     CD
 | 
			
		||||
  // -----------
 | 
			
		||||
  //                    BD    16 * 16 -> 32 bit products
 | 
			
		||||
  //                 CB
 | 
			
		||||
  //                 AD
 | 
			
		||||
  //                AC
 | 
			
		||||
  //             |----| 64 bit product
 | 
			
		||||
  int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16);
 | 
			
		||||
  uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF);
 | 
			
		||||
 | 
			
		||||
  int32_t ac = a * c;
 | 
			
		||||
  int32_t ad_cb = a * d + c * b;
 | 
			
		||||
  uint32_t bd = b * d;
 | 
			
		||||
 | 
			
		||||
  int32_t product_hi = ac + (ad_cb >> 16);  // NOLINT
 | 
			
		||||
 | 
			
		||||
  // Handle carry from lower 32 bits to upper part of result.
 | 
			
		||||
  uint32_t ad_cb_temp = ad_cb << 16;  // NOLINT
 | 
			
		||||
  uint32_t product_lo = bd + ad_cb_temp;
 | 
			
		||||
  if (product_lo < bd)
 | 
			
		||||
    product_hi++;
 | 
			
		||||
 | 
			
		||||
#ifndef FIXMATH_NO_OVERFLOW
 | 
			
		||||
  // The upper 17 bits should all be the same (the sign).
 | 
			
		||||
  if (product_hi >> 31 != product_hi >> 15)
 | 
			
		||||
    return FIX16_OVERFLOW;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef FIXMATH_NO_ROUNDING
 | 
			
		||||
  return (product_hi << 16) | (product_lo >> 16);
 | 
			
		||||
#else
 | 
			
		||||
  // Subtracting 0x8000 (= 0.5) and then using signed right shift
 | 
			
		||||
  // achieves proper rounding to result-1, except in the corner
 | 
			
		||||
  // case of negative numbers and lowest word = 0x8000.
 | 
			
		||||
  // To handle that, we also have to subtract 1 for negative numbers.
 | 
			
		||||
  uint32_t product_lo_tmp = product_lo;
 | 
			
		||||
  product_lo -= 0x8000;
 | 
			
		||||
  product_lo -= (uint32_t) product_hi >> 31;
 | 
			
		||||
  if (product_lo > product_lo_tmp)
 | 
			
		||||
    product_hi--;
 | 
			
		||||
 | 
			
		||||
  // Discard the lowest 16 bits. Note that this is not exactly the same
 | 
			
		||||
  // as dividing by 0x10000. For example if product = -1, result will
 | 
			
		||||
  // also be -1 and not 0. This is compensated by adding +1 to the result
 | 
			
		||||
  // and compensating this in turn in the rounding above.
 | 
			
		||||
  fix16_t result = (product_hi << 16) | (product_lo >> 16);  // NOLINT
 | 
			
		||||
  result += 1;
 | 
			
		||||
  return result;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t fix16_div(fix16_t a, fix16_t b) {
 | 
			
		||||
  // This uses the basic binary restoring division algorithm.
 | 
			
		||||
  // It appears to be faster to do the whole division manually than
 | 
			
		||||
  // trying to compose a 64-bit divide out of 32-bit divisions on
 | 
			
		||||
  // platforms without hardware divide.
 | 
			
		||||
 | 
			
		||||
  if (b == 0)
 | 
			
		||||
    return FIX16_MINIMUM;
 | 
			
		||||
 | 
			
		||||
  uint32_t remainder = (a >= 0) ? a : (-a);
 | 
			
		||||
  uint32_t divider = (b >= 0) ? b : (-b);
 | 
			
		||||
 | 
			
		||||
  uint32_t quotient = 0;
 | 
			
		||||
  uint32_t bit = 0x10000;
 | 
			
		||||
 | 
			
		||||
  /* The algorithm requires D >= R */
 | 
			
		||||
  while (divider < remainder) {
 | 
			
		||||
    divider <<= 1;
 | 
			
		||||
    bit <<= 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifndef FIXMATH_NO_OVERFLOW
 | 
			
		||||
  if (!bit)
 | 
			
		||||
    return FIX16_OVERFLOW;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (divider & 0x80000000) {
 | 
			
		||||
    // Perform one step manually to avoid overflows later.
 | 
			
		||||
    // We know that divider's bottom bit is 0 here.
 | 
			
		||||
    if (remainder >= divider) {
 | 
			
		||||
      quotient |= bit;
 | 
			
		||||
      remainder -= divider;
 | 
			
		||||
    }
 | 
			
		||||
    divider >>= 1;
 | 
			
		||||
    bit >>= 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Main division loop */
 | 
			
		||||
  while (bit && remainder) {
 | 
			
		||||
    if (remainder >= divider) {
 | 
			
		||||
      quotient |= bit;
 | 
			
		||||
      remainder -= divider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    remainder <<= 1;
 | 
			
		||||
    bit >>= 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifndef FIXMATH_NO_ROUNDING
 | 
			
		||||
  if (remainder >= divider) {
 | 
			
		||||
    quotient++;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  fix16_t result = quotient;
 | 
			
		||||
 | 
			
		||||
  /* Figure out the sign of result */
 | 
			
		||||
  if ((a ^ b) & 0x80000000) {
 | 
			
		||||
#ifndef FIXMATH_NO_OVERFLOW
 | 
			
		||||
    if (result == FIX16_MINIMUM)  // NOLINT(clang-diagnostic-sign-compare)
 | 
			
		||||
      return FIX16_OVERFLOW;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    result = -result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t fix16_sqrt(fix16_t in_value) {
 | 
			
		||||
  // It is assumed that x is not negative
 | 
			
		||||
 | 
			
		||||
  uint32_t num = in_value;
 | 
			
		||||
  uint32_t result = 0;
 | 
			
		||||
  uint32_t bit;
 | 
			
		||||
  uint8_t n;
 | 
			
		||||
 | 
			
		||||
  bit = (uint32_t) 1 << 30;
 | 
			
		||||
  while (bit > num)
 | 
			
		||||
    bit >>= 2;
 | 
			
		||||
 | 
			
		||||
  // The main part is executed twice, in order to avoid
 | 
			
		||||
  // using 64 bit values in computations.
 | 
			
		||||
  for (n = 0; n < 2; n++) {
 | 
			
		||||
    // First we get the top 24 bits of the answer.
 | 
			
		||||
    while (bit) {
 | 
			
		||||
      if (num >= result + bit) {
 | 
			
		||||
        num -= result + bit;
 | 
			
		||||
        result = (result >> 1) + bit;
 | 
			
		||||
      } else {
 | 
			
		||||
        result = (result >> 1);
 | 
			
		||||
      }
 | 
			
		||||
      bit >>= 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (n == 0) {
 | 
			
		||||
      // Then process it again to get the lowest 8 bits.
 | 
			
		||||
      if (num > 65535) {
 | 
			
		||||
        // The remainder 'num' is too large to be shifted left
 | 
			
		||||
        // by 16, so we have to add 1 to result manually and
 | 
			
		||||
        // adjust 'num' accordingly.
 | 
			
		||||
        // num = a - (result + 0.5)^2
 | 
			
		||||
        //     = num + result^2 - (result + 0.5)^2
 | 
			
		||||
        //     = num - result - 0.5
 | 
			
		||||
        num -= result;
 | 
			
		||||
        num = (num << 16) - 0x8000;
 | 
			
		||||
        result = (result << 16) + 0x8000;
 | 
			
		||||
      } else {
 | 
			
		||||
        num <<= 16;
 | 
			
		||||
        result <<= 16;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      bit = 1 << 14;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifndef FIXMATH_NO_ROUNDING
 | 
			
		||||
  // Finally, if next bit would have been 1, round the result upwards.
 | 
			
		||||
  if (num > result) {
 | 
			
		||||
    result++;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  return (fix16_t) result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t fix16_exp(fix16_t in_value) {
 | 
			
		||||
  // Function to approximate exp(); optimized more for code size than speed
 | 
			
		||||
 | 
			
		||||
  // exp(x) for x = +/- {1, 1/8, 1/64, 1/512}
 | 
			
		||||
  fix16_t x = in_value;
 | 
			
		||||
  static const uint8_t NUM_EXP_VALUES = 4;
 | 
			
		||||
  static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)};
 | 
			
		||||
  static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)};
 | 
			
		||||
  const fix16_t *exp_values;
 | 
			
		||||
 | 
			
		||||
  fix16_t res, arg;
 | 
			
		||||
  uint16_t i;
 | 
			
		||||
 | 
			
		||||
  if (x >= F16(10.3972))
 | 
			
		||||
    return FIX16_MAXIMUM;
 | 
			
		||||
  if (x <= F16(-11.7835))
 | 
			
		||||
    return 0;
 | 
			
		||||
 | 
			
		||||
  if (x < 0) {
 | 
			
		||||
    x = -x;
 | 
			
		||||
    exp_values = EXP_NEG_VALUES;
 | 
			
		||||
  } else {
 | 
			
		||||
    exp_values = EXP_POS_VALUES;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  res = FIX16_ONE;
 | 
			
		||||
  arg = FIX16_ONE;
 | 
			
		||||
  for (i = 0; i < NUM_EXP_VALUES; i++) {
 | 
			
		||||
    while (x >= arg) {
 | 
			
		||||
      res = fix16_mul(res, exp_values[i]);
 | 
			
		||||
      x -= arg;
 | 
			
		||||
    }
 | 
			
		||||
    arg >>= 3;
 | 
			
		||||
  }
 | 
			
		||||
  return res;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_init_instances(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
 | 
			
		||||
                                                                 fix16_t tau_mean_variance_hours,
 | 
			
		||||
                                                                 fix16_t gating_max_duration_minutes);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
 | 
			
		||||
                                                             fix16_t uptime_gamma);
 | 
			
		||||
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params);
 | 
			
		||||
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
 | 
			
		||||
                                                                  fix16_t voc_index_from_prior);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
 | 
			
		||||
                                                          fix16_t voc_index_from_prior);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
 | 
			
		||||
                                                                         fix16_t x0, fix16_t k);
 | 
			
		||||
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample);
 | 
			
		||||
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean);
 | 
			
		||||
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw);
 | 
			
		||||
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset);
 | 
			
		||||
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample);
 | 
			
		||||
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params);
 | 
			
		||||
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params);
 | 
			
		||||
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample);
 | 
			
		||||
 | 
			
		||||
void voc_algorithm_init(VocAlgorithmParams *params) {
 | 
			
		||||
  params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT);
 | 
			
		||||
  params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS);
 | 
			
		||||
  params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES);
 | 
			
		||||
  params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL);
 | 
			
		||||
  params->mUptime = F16(0.);
 | 
			
		||||
  params->mSraw = F16(0.);
 | 
			
		||||
  params->mVoc_Index = 0;
 | 
			
		||||
  voc_algorithm_init_instances(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_init_instances(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_init(params);
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_set_parameters(
 | 
			
		||||
      params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes);
 | 
			
		||||
  voc_algorithm_mox_model_init(params);
 | 
			
		||||
  voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
 | 
			
		||||
                                         voc_algorithm_mean_variance_estimator_get_mean(params));
 | 
			
		||||
  voc_algorithm_sigmoid_scaled_init(params);
 | 
			
		||||
  voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset);
 | 
			
		||||
  voc_algorithm_adaptive_lowpass_init(params);
 | 
			
		||||
  voc_algorithm_adaptive_lowpass_set_parameters(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) {
 | 
			
		||||
  *state0 = voc_algorithm_mean_variance_estimator_get_mean(params);
 | 
			
		||||
  *state1 = voc_algorithm_mean_variance_estimator_get_std(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) {
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA));
 | 
			
		||||
  params->mSraw = state0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
 | 
			
		||||
                                         int32_t learning_time_hours, int32_t gating_max_duration_minutes,
 | 
			
		||||
                                         int32_t std_initial) {
 | 
			
		||||
  params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset));
 | 
			
		||||
  params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours));
 | 
			
		||||
  params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes));
 | 
			
		||||
  params->mSraw_Std_Initial = (fix16_from_int(std_initial));
 | 
			
		||||
  voc_algorithm_init_instances(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) {
 | 
			
		||||
  if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) {
 | 
			
		||||
    params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
 | 
			
		||||
  } else {
 | 
			
		||||
    if (((sraw > 0) && (sraw < 65000))) {
 | 
			
		||||
      if ((sraw < 20001)) {
 | 
			
		||||
        sraw = 20001;
 | 
			
		||||
      } else if ((sraw > 52767)) {
 | 
			
		||||
        sraw = 52767;
 | 
			
		||||
      }
 | 
			
		||||
      params->mSraw = (fix16_from_int((sraw - 20000)));
 | 
			
		||||
    }
 | 
			
		||||
    params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw);
 | 
			
		||||
    params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index);
 | 
			
		||||
    params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index);
 | 
			
		||||
    if ((params->mVoc_Index < F16(0.5))) {
 | 
			
		||||
      params->mVoc_Index = F16(0.5);
 | 
			
		||||
    }
 | 
			
		||||
    if ((params->mSraw > F16(0.))) {
 | 
			
		||||
      voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index);
 | 
			
		||||
      voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
 | 
			
		||||
                                             voc_algorithm_mean_variance_estimator_get_mean(params));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5))));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.));
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_init_instances(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_sigmoid_init(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
 | 
			
		||||
                                                                 fix16_t tau_mean_variance_hours,
 | 
			
		||||
                                                                 fix16_t gating_max_duration_minutes) {
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Initialized = false;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Mean = F16(0.);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Std = std_initial;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma =
 | 
			
		||||
      (fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))),
 | 
			
		||||
                 (tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.)))));
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma_Initial_Mean =
 | 
			
		||||
      F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
 | 
			
		||||
           (VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL)));
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma_Initial_Variance =
 | 
			
		||||
      F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
 | 
			
		||||
           (VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL)));
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
 | 
			
		||||
                                                             fix16_t uptime_gamma) {
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Mean = mean;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Std = std;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Initialized = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) {
 | 
			
		||||
  return params->m_Mean_Variance_Estimator_Std;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) {
 | 
			
		||||
  return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
 | 
			
		||||
                                                                  fix16_t voc_index_from_prior) {
 | 
			
		||||
  fix16_t uptime_limit;
 | 
			
		||||
  fix16_t sigmoid_gamma_mean;
 | 
			
		||||
  fix16_t gamma_mean;
 | 
			
		||||
  fix16_t gating_threshold_mean;
 | 
			
		||||
  fix16_t sigmoid_gating_mean;
 | 
			
		||||
  fix16_t sigmoid_gamma_variance;
 | 
			
		||||
  fix16_t gamma_variance;
 | 
			
		||||
  fix16_t gating_threshold_variance;
 | 
			
		||||
  fix16_t sigmoid_gating_variance;
 | 
			
		||||
 | 
			
		||||
  uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL));
 | 
			
		||||
  if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) {
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Uptime_Gamma =
 | 
			
		||||
        (params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
 | 
			
		||||
  }
 | 
			
		||||
  if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) {
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Uptime_Gating =
 | 
			
		||||
        (params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
 | 
			
		||||
  }
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN),
 | 
			
		||||
                                                               F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN));
 | 
			
		||||
  sigmoid_gamma_mean =
 | 
			
		||||
      voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
 | 
			
		||||
  gamma_mean =
 | 
			
		||||
      (params->m_Mean_Variance_Estimator_Gamma +
 | 
			
		||||
       (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma),
 | 
			
		||||
                  sigmoid_gamma_mean)));
 | 
			
		||||
  gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) +
 | 
			
		||||
                           (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
 | 
			
		||||
                                      voc_algorithm_mean_variance_estimator_sigmoid_process(
 | 
			
		||||
                                          params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean,
 | 
			
		||||
                                                               F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
 | 
			
		||||
  sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean));
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(
 | 
			
		||||
      params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE));
 | 
			
		||||
  sigmoid_gamma_variance =
 | 
			
		||||
      voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
 | 
			
		||||
  gamma_variance =
 | 
			
		||||
      (params->m_Mean_Variance_Estimator_Gamma +
 | 
			
		||||
       (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma),
 | 
			
		||||
                  (sigmoid_gamma_variance - sigmoid_gamma_mean))));
 | 
			
		||||
  gating_threshold_variance =
 | 
			
		||||
      (F16(VOC_ALGORITHM_GATING_THRESHOLD) +
 | 
			
		||||
       (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
 | 
			
		||||
                  voc_algorithm_mean_variance_estimator_sigmoid_process(
 | 
			
		||||
                      params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance,
 | 
			
		||||
                                                               F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
 | 
			
		||||
  sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance));
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Gating_Duration_Minutes =
 | 
			
		||||
      (params->m_Mean_Variance_Estimator_Gating_Duration_Minutes +
 | 
			
		||||
       (fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)),
 | 
			
		||||
                  ((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) -
 | 
			
		||||
                   F16(VOC_ALGORITHM_GATING_MAX_RATIO)))));
 | 
			
		||||
  if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) {
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
 | 
			
		||||
  }
 | 
			
		||||
  if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes >
 | 
			
		||||
       params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) {
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
 | 
			
		||||
                                                          fix16_t voc_index_from_prior) {
 | 
			
		||||
  fix16_t delta_sgp;
 | 
			
		||||
  fix16_t c;
 | 
			
		||||
  fix16_t additional_scaling;
 | 
			
		||||
 | 
			
		||||
  if ((!params->m_Mean_Variance_Estimator_Initialized)) {
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Initialized = true;
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Sraw_Offset = sraw;
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Mean = F16(0.);
 | 
			
		||||
  } else {
 | 
			
		||||
    if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) ||
 | 
			
		||||
         (params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) {
 | 
			
		||||
      params->m_Mean_Variance_Estimator_Sraw_Offset =
 | 
			
		||||
          (params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean);
 | 
			
		||||
      params->m_Mean_Variance_Estimator_Mean = F16(0.);
 | 
			
		||||
    }
 | 
			
		||||
    sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset);
 | 
			
		||||
    voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior);
 | 
			
		||||
    delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean),
 | 
			
		||||
                           F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING)));
 | 
			
		||||
    if ((delta_sgp < F16(0.))) {
 | 
			
		||||
      c = (params->m_Mean_Variance_Estimator_Std - delta_sgp);
 | 
			
		||||
    } else {
 | 
			
		||||
      c = (params->m_Mean_Variance_Estimator_Std + delta_sgp);
 | 
			
		||||
    }
 | 
			
		||||
    additional_scaling = F16(1.);
 | 
			
		||||
    if ((c > F16(1440.))) {
 | 
			
		||||
      additional_scaling = F16(4.);
 | 
			
		||||
    }
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Std = (fix16_mul(
 | 
			
		||||
        fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) -
 | 
			
		||||
                                                   params->m_Mean_Variance_Estimator_Gamma_Variance)))),
 | 
			
		||||
        fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std,
 | 
			
		||||
                               (fix16_div(params->m_Mean_Variance_Estimator_Std,
 | 
			
		||||
                                          (fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING),
 | 
			
		||||
                                                     additional_scaling)))))) +
 | 
			
		||||
                    (fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)),
 | 
			
		||||
                                          additional_scaling)),
 | 
			
		||||
                               delta_sgp))))));
 | 
			
		||||
    params->m_Mean_Variance_Estimator_Mean =
 | 
			
		||||
        (params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp)));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
 | 
			
		||||
                                                                         fix16_t x0, fix16_t k) {
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Sigmoid_L = l;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Sigmoid_K = k;
 | 
			
		||||
  params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) {
 | 
			
		||||
  fix16_t x;
 | 
			
		||||
 | 
			
		||||
  x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0)));
 | 
			
		||||
  if ((x < F16(-50.))) {
 | 
			
		||||
    return params->m_Mean_Variance_Estimator_Sigmoid_L;
 | 
			
		||||
  } else if ((x > F16(50.))) {
 | 
			
		||||
    return F16(0.);
 | 
			
		||||
  } else {
 | 
			
		||||
    return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x))));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) {
 | 
			
		||||
  params->m_Mox_Model_Sraw_Std = sraw_std;
 | 
			
		||||
  params->m_Mox_Model_Sraw_Mean = sraw_mean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) {
 | 
			
		||||
  return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean),
 | 
			
		||||
                               (-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))),
 | 
			
		||||
                    F16(VOC_ALGORITHM_VOC_INDEX_GAIN)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) {
 | 
			
		||||
  params->m_Sigmoid_Scaled_Offset = offset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) {
 | 
			
		||||
  fix16_t x;
 | 
			
		||||
  fix16_t shift;
 | 
			
		||||
 | 
			
		||||
  x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0))));
 | 
			
		||||
  if ((x < F16(-50.))) {
 | 
			
		||||
    return F16(VOC_ALGORITHM_SIGMOID_L);
 | 
			
		||||
  } else if ((x > F16(50.))) {
 | 
			
		||||
    return F16(0.);
 | 
			
		||||
  } else {
 | 
			
		||||
    if ((sample >= F16(0.))) {
 | 
			
		||||
      shift =
 | 
			
		||||
          (fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.)));
 | 
			
		||||
      return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift);
 | 
			
		||||
    } else {
 | 
			
		||||
      return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))),
 | 
			
		||||
                        (fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x))))));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) {
 | 
			
		||||
  voc_algorithm_adaptive_lowpass_set_parameters(params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) {
 | 
			
		||||
  params->m_Adaptive_Lowpass_A1 =
 | 
			
		||||
      F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL)));
 | 
			
		||||
  params->m_Adaptive_Lowpass_A2 =
 | 
			
		||||
      F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL)));
 | 
			
		||||
  params->m_Adaptive_Lowpass_Initialized = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) {
 | 
			
		||||
  fix16_t abs_delta;
 | 
			
		||||
  fix16_t f1;
 | 
			
		||||
  fix16_t tau_a;
 | 
			
		||||
  fix16_t a3;
 | 
			
		||||
 | 
			
		||||
  if ((!params->m_Adaptive_Lowpass_Initialized)) {
 | 
			
		||||
    params->m_Adaptive_Lowpass_X1 = sample;
 | 
			
		||||
    params->m_Adaptive_Lowpass_X2 = sample;
 | 
			
		||||
    params->m_Adaptive_Lowpass_X3 = sample;
 | 
			
		||||
    params->m_Adaptive_Lowpass_Initialized = true;
 | 
			
		||||
  }
 | 
			
		||||
  params->m_Adaptive_Lowpass_X1 =
 | 
			
		||||
      ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) +
 | 
			
		||||
       (fix16_mul(params->m_Adaptive_Lowpass_A1, sample)));
 | 
			
		||||
  params->m_Adaptive_Lowpass_X2 =
 | 
			
		||||
      ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) +
 | 
			
		||||
       (fix16_mul(params->m_Adaptive_Lowpass_A2, sample)));
 | 
			
		||||
  abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2);
 | 
			
		||||
  if ((abs_delta < F16(0.))) {
 | 
			
		||||
    abs_delta = (-abs_delta);
 | 
			
		||||
  }
 | 
			
		||||
  f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta)));
 | 
			
		||||
  tau_a =
 | 
			
		||||
      ((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST));
 | 
			
		||||
  a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a)));
 | 
			
		||||
  params->m_Adaptive_Lowpass_X3 =
 | 
			
		||||
      ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample)));
 | 
			
		||||
  return params->m_Adaptive_Lowpass_X3;
 | 
			
		||||
}
 | 
			
		||||
}  // namespace sgp40
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,147 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sgp40 {
 | 
			
		||||
 | 
			
		||||
/* The VOC code were originally created by
 | 
			
		||||
 *  https://github.com/Sensirion/embedded-sgp
 | 
			
		||||
 * The fixed point arithmetic parts of this code were originally created by
 | 
			
		||||
 * https://github.com/PetteriAimonen/libfixmath
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
using fix16_t = int32_t;
 | 
			
		||||
 | 
			
		||||
#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5)))
 | 
			
		||||
 | 
			
		||||
static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.);
 | 
			
		||||
static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.);
 | 
			
		||||
static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.);
 | 
			
		||||
static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.);
 | 
			
		||||
static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.);
 | 
			
		||||
static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.);
 | 
			
		||||
static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.);
 | 
			
		||||
static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75));
 | 
			
		||||
static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01);
 | 
			
		||||
static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.);
 | 
			
		||||
static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45));
 | 
			
		||||
static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01);
 | 
			
		||||
static const float VOC_ALGORITHM_GATING_THRESHOLD(340.);
 | 
			
		||||
static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.);
 | 
			
		||||
static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09);
 | 
			
		||||
static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.));
 | 
			
		||||
static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3);
 | 
			
		||||
static const float VOC_ALGORITHM_SIGMOID_L(500.);
 | 
			
		||||
static const float VOC_ALGORITHM_SIGMOID_K(-0.0065);
 | 
			
		||||
static const float VOC_ALGORITHM_SIGMOID_X0(213.);
 | 
			
		||||
static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.);
 | 
			
		||||
static const float VOC_ALGORITHM_LP_TAU_FAST(20.0);
 | 
			
		||||
static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0);
 | 
			
		||||
static const float VOC_ALGORITHM_LP_ALPHA(-0.2);
 | 
			
		||||
static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.));
 | 
			
		||||
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.);
 | 
			
		||||
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Struct to hold all the states of the VOC algorithm.
 | 
			
		||||
 */
 | 
			
		||||
struct VocAlgorithmParams {
 | 
			
		||||
  fix16_t mVoc_Index_Offset;
 | 
			
		||||
  fix16_t mTau_Mean_Variance_Hours;
 | 
			
		||||
  fix16_t mGating_Max_Duration_Minutes;
 | 
			
		||||
  fix16_t mSraw_Std_Initial;
 | 
			
		||||
  fix16_t mUptime;
 | 
			
		||||
  fix16_t mSraw;
 | 
			
		||||
  fix16_t mVoc_Index;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes;
 | 
			
		||||
  bool m_Mean_Variance_Estimator_Initialized;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Mean;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Sraw_Offset;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Std;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gamma;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gamma_Mean;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gamma_Variance;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Uptime_Gamma;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Uptime_Gating;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Sigmoid_L;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Sigmoid_K;
 | 
			
		||||
  fix16_t m_Mean_Variance_Estimator_Sigmoid_X0;
 | 
			
		||||
  fix16_t m_Mox_Model_Sraw_Std;
 | 
			
		||||
  fix16_t m_Mox_Model_Sraw_Mean;
 | 
			
		||||
  fix16_t m_Sigmoid_Scaled_Offset;
 | 
			
		||||
  fix16_t m_Adaptive_Lowpass_A1;
 | 
			
		||||
  fix16_t m_Adaptive_Lowpass_A2;
 | 
			
		||||
  bool m_Adaptive_Lowpass_Initialized;
 | 
			
		||||
  fix16_t m_Adaptive_Lowpass_X1;
 | 
			
		||||
  fix16_t m_Adaptive_Lowpass_X2;
 | 
			
		||||
  fix16_t m_Adaptive_Lowpass_X3;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Initialize the VOC algorithm parameters. Call this once at the beginning or
 | 
			
		||||
 * whenever the sensor stopped measurements.
 | 
			
		||||
 * @param params    Pointer to the VocAlgorithmParams struct
 | 
			
		||||
 */
 | 
			
		||||
void voc_algorithm_init(VocAlgorithmParams *params);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get current algorithm states. Retrieved values can be used in
 | 
			
		||||
 * voc_algorithm_set_states() to resume operation after a short interruption,
 | 
			
		||||
 * skipping initial learning phase. This feature can only be used after at least
 | 
			
		||||
 * 3 hours of continuous operation.
 | 
			
		||||
 * @param params    Pointer to the VocAlgorithmParams struct
 | 
			
		||||
 * @param state0    State0 to be stored
 | 
			
		||||
 * @param state1    State1 to be stored
 | 
			
		||||
 */
 | 
			
		||||
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set previously retrieved algorithm states to resume operation after a short
 | 
			
		||||
 * interruption, skipping initial learning phase. This feature should not be
 | 
			
		||||
 * used after inerruptions of more than 10 minutes. Call this once after
 | 
			
		||||
 * voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if
 | 
			
		||||
 * desired. Otherwise, the algorithm will start with initial learning phase.
 | 
			
		||||
 * @param params    Pointer to the VocAlgorithmParams struct
 | 
			
		||||
 * @param state0    State0 to be restored
 | 
			
		||||
 * @param state1    State1 to be restored
 | 
			
		||||
 */
 | 
			
		||||
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set parameters to customize the VOC algorithm. Call this once after
 | 
			
		||||
 * voc_algorithm_init(), if desired. Otherwise, the default values will be used.
 | 
			
		||||
 *
 | 
			
		||||
 * @param params                      Pointer to the VocAlgorithmParams struct
 | 
			
		||||
 * @param voc_index_offset            VOC index representing typical (average)
 | 
			
		||||
 *                                    conditions. Range 1..250, default 100
 | 
			
		||||
 * @param learning_time_hours         Time constant of long-term estimator.
 | 
			
		||||
 *                                    Past events will be forgotten after about
 | 
			
		||||
 *                                    twice the learning time.
 | 
			
		||||
 *                                    Range 1..72 [hours], default 12 [hours]
 | 
			
		||||
 * @param gating_max_duration_minutes Maximum duration of gating (freeze of
 | 
			
		||||
 *                                    estimator during high VOC index signal).
 | 
			
		||||
 *                                    0 (no gating) or range 1..720 [minutes],
 | 
			
		||||
 *                                    default 180 [minutes]
 | 
			
		||||
 * @param std_initial                 Initial estimate for standard deviation.
 | 
			
		||||
 *                                    Lower value boosts events during initial
 | 
			
		||||
 *                                    learning period, but may result in larger
 | 
			
		||||
 *                                    device-to-device variations.
 | 
			
		||||
 *                                    Range 10..500, default 50
 | 
			
		||||
 */
 | 
			
		||||
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
 | 
			
		||||
                                         int32_t learning_time_hours, int32_t gating_max_duration_minutes,
 | 
			
		||||
                                         int32_t std_initial);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculate the VOC index value from the raw sensor value.
 | 
			
		||||
 *
 | 
			
		||||
 * @param params    Pointer to the VocAlgorithmParams struct
 | 
			
		||||
 * @param sraw      Raw value from the SGP40 sensor
 | 
			
		||||
 * @param voc_index Calculated VOC index value from the raw sensor value. Zero
 | 
			
		||||
 *                  during initial blackout period and 1..500 afterwards
 | 
			
		||||
 */
 | 
			
		||||
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index);
 | 
			
		||||
}  // namespace sgp40
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,70 +1,8 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor, sensirion_common
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_STORE_BASELINE,
 | 
			
		||||
    CONF_TEMPERATURE_SOURCE,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
AUTO_LOAD = ["sensirion_common"]
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@SenexCrenshaw"]
 | 
			
		||||
 | 
			
		||||
sgp40_ns = cg.esphome_ns.namespace("sgp40")
 | 
			
		||||
SGP40Component = sgp40_ns.class_(
 | 
			
		||||
    "SGP40Component",
 | 
			
		||||
    sensor.Sensor,
 | 
			
		||||
    cg.PollingComponent,
 | 
			
		||||
    sensirion_common.SensirionI2CDevice,
 | 
			
		||||
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
 | 
			
		||||
    "SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n"
 | 
			
		||||
    " See https://esphome.io/components/sensor/sgp4x.html"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_COMPENSATION = "compensation"
 | 
			
		||||
CONF_HUMIDITY_SOURCE = "humidity_source"
 | 
			
		||||
CONF_VOC_BASELINE = "voc_baseline"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        SGP40Component,
 | 
			
		||||
        icon=ICON_RADIATOR,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
 | 
			
		||||
            cv.Optional(CONF_COMPENSATION): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
                    cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
                },
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x59))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_COMPENSATION in config:
 | 
			
		||||
        compensation_config = config[CONF_COMPENSATION]
 | 
			
		||||
        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
 | 
			
		||||
 | 
			
		||||
    if CONF_VOC_BASELINE in config:
 | 
			
		||||
        cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,274 +0,0 @@
 | 
			
		||||
#include "sgp40.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sgp40 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "sgp40";
 | 
			
		||||
 | 
			
		||||
void SGP40Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up SGP40...");
 | 
			
		||||
 | 
			
		||||
  // Serial Number identification
 | 
			
		||||
  if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint16_t raw_serial_number[3];
 | 
			
		||||
 | 
			
		||||
  if (!this->read_data(raw_serial_number, 3)) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
 | 
			
		||||
                         (uint64_t(raw_serial_number[2]));
 | 
			
		||||
  ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
 | 
			
		||||
 | 
			
		||||
  // Featureset identification for future use
 | 
			
		||||
  if (!this->write_command(SGP40_CMD_GET_FEATURESET)) {
 | 
			
		||||
    ESP_LOGD(TAG, "raw_featureset write_command_ failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint16_t raw_featureset;
 | 
			
		||||
  if (!this->read_data(raw_featureset)) {
 | 
			
		||||
    ESP_LOGD(TAG, "raw_featureset read_data_ failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->featureset_ = raw_featureset;
 | 
			
		||||
  if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
 | 
			
		||||
    ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
 | 
			
		||||
             SGP40_FEATURESET);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
 | 
			
		||||
 | 
			
		||||
  voc_algorithm_init(&this->voc_algorithm_params_);
 | 
			
		||||
 | 
			
		||||
  if (this->store_baseline_) {
 | 
			
		||||
    // Hash with compilation time
 | 
			
		||||
    // This ensures the baseline storage is cleared after OTA
 | 
			
		||||
    uint32_t hash = fnv1_hash(App.get_compilation_time());
 | 
			
		||||
    this->pref_ = global_preferences->make_preference<SGP40Baselines>(hash, true);
 | 
			
		||||
 | 
			
		||||
    if (this->pref_.load(&this->baselines_storage_)) {
 | 
			
		||||
      this->state0_ = this->baselines_storage_.state0;
 | 
			
		||||
      this->state1_ = this->baselines_storage_.state1;
 | 
			
		||||
      ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
 | 
			
		||||
               baselines_storage_.state1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize storage timestamp
 | 
			
		||||
    this->seconds_since_last_store_ = 0;
 | 
			
		||||
 | 
			
		||||
    if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) {
 | 
			
		||||
      ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
 | 
			
		||||
               baselines_storage_.state1);
 | 
			
		||||
      voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0,
 | 
			
		||||
                               this->baselines_storage_.state1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->self_test_();
 | 
			
		||||
 | 
			
		||||
  /* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf
 | 
			
		||||
  indicates this sensor should be driven at 1Hz. Comments from the developers at:
 | 
			
		||||
  https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit
 | 
			
		||||
  resilient to slight timing variations so the software timer should be accurate enough for
 | 
			
		||||
  this.
 | 
			
		||||
 | 
			
		||||
  This block starts sampling from the sensor at 1Hz, and is done seperately from the call
 | 
			
		||||
  to the update method. This seperation is to support getting accurate measurements but
 | 
			
		||||
  limit the amount of communication done over wifi for power consumption or to keep the
 | 
			
		||||
  number of records reported from being overwhelming.
 | 
			
		||||
  */
 | 
			
		||||
  ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
 | 
			
		||||
  this->set_interval(1000, [this]() { this->update_voc_index(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP40Component::self_test_() {
 | 
			
		||||
  ESP_LOGD(TAG, "Self-test started");
 | 
			
		||||
  if (!this->write_command(SGP40_CMD_SELF_TEST)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    ESP_LOGD(TAG, "Self-test communication failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->set_timeout(250, [this]() {
 | 
			
		||||
    uint16_t reply;
 | 
			
		||||
    if (!this->read_data(reply)) {
 | 
			
		||||
      ESP_LOGD(TAG, "Self-test read_data_ failed");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (reply == 0xD400) {
 | 
			
		||||
      this->self_test_complete_ = true;
 | 
			
		||||
      ESP_LOGD(TAG, "Self-test completed");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGD(TAG, "Self-test failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Combined the measured gasses, temperature, and humidity
 | 
			
		||||
 * to calculate the VOC Index
 | 
			
		||||
 *
 | 
			
		||||
 * @param temperature The measured temperature in degrees C
 | 
			
		||||
 * @param humidity The measured relative humidity in % rH
 | 
			
		||||
 * @return int32_t The VOC Index
 | 
			
		||||
 */
 | 
			
		||||
int32_t SGP40Component::measure_voc_index_() {
 | 
			
		||||
  int32_t voc_index;
 | 
			
		||||
 | 
			
		||||
  uint16_t sraw = measure_raw_();
 | 
			
		||||
 | 
			
		||||
  if (sraw == UINT16_MAX)
 | 
			
		||||
    return UINT16_MAX;
 | 
			
		||||
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
 | 
			
		||||
  voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index);
 | 
			
		||||
 | 
			
		||||
  // Store baselines after defined interval or if the difference between current and stored baseline becomes too
 | 
			
		||||
  // much
 | 
			
		||||
  if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
 | 
			
		||||
    voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_);
 | 
			
		||||
    if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF ||
 | 
			
		||||
        (uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) {
 | 
			
		||||
      this->seconds_since_last_store_ = 0;
 | 
			
		||||
      this->baselines_storage_.state0 = this->state0_;
 | 
			
		||||
      this->baselines_storage_.state1 = this->state1_;
 | 
			
		||||
 | 
			
		||||
      if (this->pref_.save(&this->baselines_storage_)) {
 | 
			
		||||
        ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0,
 | 
			
		||||
                 baselines_storage_.state1);
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Could not store VOC baselines");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return voc_index;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Return the raw gas measurement
 | 
			
		||||
 *
 | 
			
		||||
 * @param temperature The measured temperature in degrees C
 | 
			
		||||
 * @param humidity The measured relative humidity in % rH
 | 
			
		||||
 * @return uint16_t The current raw gas measurement
 | 
			
		||||
 */
 | 
			
		||||
uint16_t SGP40Component::measure_raw_() {
 | 
			
		||||
  float humidity = NAN;
 | 
			
		||||
 | 
			
		||||
  if (!this->self_test_complete_) {
 | 
			
		||||
    ESP_LOGD(TAG, "Self-test not yet complete");
 | 
			
		||||
    return UINT16_MAX;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
    humidity = this->humidity_sensor_->state;
 | 
			
		||||
  }
 | 
			
		||||
  if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
 | 
			
		||||
    humidity = 50;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float temperature = NAN;
 | 
			
		||||
  if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
    temperature = float(this->temperature_sensor_->state);
 | 
			
		||||
  }
 | 
			
		||||
  if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
 | 
			
		||||
    temperature = 25;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t data[2];
 | 
			
		||||
  uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
 | 
			
		||||
  uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
 | 
			
		||||
  // first paramater is the relative humidity ticks
 | 
			
		||||
  data[0] = rhticks;
 | 
			
		||||
  // second paramater is the temperature ticks
 | 
			
		||||
  data[1] = tempticks;
 | 
			
		||||
 | 
			
		||||
  if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    ESP_LOGD(TAG, "write error (%d)", this->last_error_);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  delay(30);
 | 
			
		||||
 | 
			
		||||
  uint16_t raw_data;
 | 
			
		||||
  if (!this->read_data(raw_data)) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    ESP_LOGD(TAG, "read_data_ error");
 | 
			
		||||
    return UINT16_MAX;
 | 
			
		||||
  }
 | 
			
		||||
  return raw_data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP40Component::update_voc_index() {
 | 
			
		||||
  this->seconds_since_last_store_ += 1;
 | 
			
		||||
 | 
			
		||||
  this->voc_index_ = this->measure_voc_index_();
 | 
			
		||||
  if (this->samples_read_ < this->samples_to_stabalize_) {
 | 
			
		||||
    this->samples_read_++;
 | 
			
		||||
    ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
 | 
			
		||||
             this->samples_to_stabalize_, this->voc_index_);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP40Component::update() {
 | 
			
		||||
  if (this->samples_read_ < this->samples_to_stabalize_) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->voc_index_ != UINT16_MAX) {
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
    this->publish_state(this->voc_index_);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP40Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "SGP40:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  store_baseline: %d", this->store_baseline_);
 | 
			
		||||
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    switch (this->error_code_) {
 | 
			
		||||
      case COMMUNICATION_FAILED:
 | 
			
		||||
        ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        ESP_LOGW(TAG, "Unknown setup error!");
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Serial number: %" PRIu64, this->serial_number_);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT);
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Compensation:");
 | 
			
		||||
    LOG_SENSOR("    ", "Temperature Source:", this->temperature_sensor_);
 | 
			
		||||
    LOG_SENSOR("    ", "Humidity Source:", this->humidity_sensor_);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Compensation: No source configured");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace sgp40
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,93 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "sensirion_voc_algorithm.h"
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sgp40 {
 | 
			
		||||
 | 
			
		||||
struct SGP40Baselines {
 | 
			
		||||
  int32_t state0;
 | 
			
		||||
  int32_t state1;
 | 
			
		||||
} PACKED;  // NOLINT
 | 
			
		||||
 | 
			
		||||
// commands and constants
 | 
			
		||||
static const uint8_t SGP40_FEATURESET = 0x0020;     ///< The required set for this library
 | 
			
		||||
static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31;  ///< Seed for SGP40's CRC polynomial
 | 
			
		||||
static const uint8_t SGP40_CRC8_INIT = 0xFF;        ///< Init value for CRC
 | 
			
		||||
static const uint8_t SGP40_WORD_LEN = 2;            ///< 2 bytes per word
 | 
			
		||||
 | 
			
		||||
// Commands
 | 
			
		||||
 | 
			
		||||
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
 | 
			
		||||
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
 | 
			
		||||
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
 | 
			
		||||
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
 | 
			
		||||
 | 
			
		||||
// Shortest time interval of 3H for storing baseline values.
 | 
			
		||||
// Prevents wear of the flash because of too many write operations
 | 
			
		||||
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
 | 
			
		||||
 | 
			
		||||
// Store anyway if the baseline difference exceeds the max storage diff value
 | 
			
		||||
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
 | 
			
		||||
 | 
			
		||||
class SGP40Component;
 | 
			
		||||
 | 
			
		||||
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
 | 
			
		||||
class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void update_voc_index();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Input sensor for humidity and temperature compensation.
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  int16_t sensirion_init_sensors_();
 | 
			
		||||
  int16_t sgp40_probe_();
 | 
			
		||||
  uint64_t serial_number_;
 | 
			
		||||
  uint16_t featureset_;
 | 
			
		||||
  int32_t measure_voc_index_();
 | 
			
		||||
  uint8_t generate_crc_(const uint8_t *data, uint8_t datalen);
 | 
			
		||||
  uint16_t measure_raw_();
 | 
			
		||||
  ESPPreferenceObject pref_;
 | 
			
		||||
  uint32_t seconds_since_last_store_;
 | 
			
		||||
  SGP40Baselines baselines_storage_;
 | 
			
		||||
  VocAlgorithmParams voc_algorithm_params_;
 | 
			
		||||
  bool self_test_complete_;
 | 
			
		||||
  bool store_baseline_;
 | 
			
		||||
  int32_t state0_;
 | 
			
		||||
  int32_t state1_;
 | 
			
		||||
  int32_t voc_index_ = 0;
 | 
			
		||||
  uint8_t samples_read_ = 0;
 | 
			
		||||
  uint8_t samples_to_stabalize_ = static_cast<int8_t>(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Request the sensor to perform a self-test, returning the result
 | 
			
		||||
   *
 | 
			
		||||
   * @return true: success false:failure
 | 
			
		||||
   */
 | 
			
		||||
  void self_test_();
 | 
			
		||||
  enum ErrorCode {
 | 
			
		||||
    COMMUNICATION_FAILED,
 | 
			
		||||
    MEASUREMENT_INIT_FAILED,
 | 
			
		||||
    INVALID_ID,
 | 
			
		||||
    UNSUPPORTED_ID,
 | 
			
		||||
    UNKNOWN
 | 
			
		||||
  } error_code_{UNKNOWN};
 | 
			
		||||
};
 | 
			
		||||
}  // namespace sgp40
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										0
									
								
								esphome/components/sgp4x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/sgp4x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										144
									
								
								esphome/components/sgp4x/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								esphome/components/sgp4x/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor, sensirion_common
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_STORE_BASELINE,
 | 
			
		||||
    CONF_TEMPERATURE_SOURCE,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
    DEVICE_CLASS_NITROUS_OXIDE,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
AUTO_LOAD = ["sensirion_common"]
 | 
			
		||||
CODEOWNERS = ["@SenexCrenshaw", "@martgras"]
 | 
			
		||||
 | 
			
		||||
sgp4x_ns = cg.esphome_ns.namespace("sgp4x")
 | 
			
		||||
SGP4xComponent = sgp4x_ns.class_(
 | 
			
		||||
    "SGP4xComponent",
 | 
			
		||||
    sensor.Sensor,
 | 
			
		||||
    cg.PollingComponent,
 | 
			
		||||
    sensirion_common.SensirionI2CDevice,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_ALGORITHM_TUNING = "algorithm_tuning"
 | 
			
		||||
CONF_COMPENSATION = "compensation"
 | 
			
		||||
CONF_GAIN_FACTOR = "gain_factor"
 | 
			
		||||
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
 | 
			
		||||
CONF_HUMIDITY_SOURCE = "humidity_source"
 | 
			
		||||
CONF_INDEX_OFFSET = "index_offset"
 | 
			
		||||
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
 | 
			
		||||
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
 | 
			
		||||
CONF_NOX = "nox"
 | 
			
		||||
CONF_STD_INITIAL = "std_initial"
 | 
			
		||||
CONF_VOC = "voc"
 | 
			
		||||
CONF_VOC_BASELINE = "voc_baseline"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_sensors(config):
 | 
			
		||||
    if CONF_VOC not in config and CONF_NOX not in config:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}"
 | 
			
		||||
        )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
GAS_SENSOR = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_,
 | 
			
		||||
                cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_,
 | 
			
		||||
                cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_,
 | 
			
		||||
                cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_,
 | 
			
		||||
                cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
 | 
			
		||||
                cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(SGP4xComponent),
 | 
			
		||||
            cv.Optional(CONF_VOC): sensor.sensor_schema(
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ).extend(GAS_SENSOR),
 | 
			
		||||
            cv.Optional(CONF_NOX): sensor.sensor_schema(
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_NITROUS_OXIDE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ).extend(GAS_SENSOR),
 | 
			
		||||
            cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
 | 
			
		||||
            cv.Optional(CONF_COMPENSATION): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
                    cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
                },
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x59)),
 | 
			
		||||
    validate_sensors,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 CONF_COMPENSATION in config:
 | 
			
		||||
        compensation_config = config[CONF_COMPENSATION]
 | 
			
		||||
        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
 | 
			
		||||
 | 
			
		||||
    if CONF_VOC_BASELINE in config:
 | 
			
		||||
        cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))
 | 
			
		||||
 | 
			
		||||
    if CONF_VOC in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_VOC])
 | 
			
		||||
        cg.add(var.set_voc_sensor(sens))
 | 
			
		||||
        if CONF_ALGORITHM_TUNING in config[CONF_VOC]:
 | 
			
		||||
            cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
 | 
			
		||||
            cg.add(
 | 
			
		||||
                var.set_voc_algorithm_tuning(
 | 
			
		||||
                    cfg[CONF_INDEX_OFFSET],
 | 
			
		||||
                    cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
 | 
			
		||||
                    cfg[CONF_LEARNING_TIME_GAIN_HOURS],
 | 
			
		||||
                    cfg[CONF_GATING_MAX_DURATION_MINUTES],
 | 
			
		||||
                    cfg[CONF_STD_INITIAL],
 | 
			
		||||
                    cfg[CONF_GAIN_FACTOR],
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    if CONF_NOX in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_NOX])
 | 
			
		||||
        cg.add(var.set_nox_sensor(sens))
 | 
			
		||||
        if CONF_ALGORITHM_TUNING in config[CONF_NOX]:
 | 
			
		||||
            cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
 | 
			
		||||
            cg.add(
 | 
			
		||||
                var.set_nox_algorithm_tuning(
 | 
			
		||||
                    cfg[CONF_INDEX_OFFSET],
 | 
			
		||||
                    cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
 | 
			
		||||
                    cfg[CONF_LEARNING_TIME_GAIN_HOURS],
 | 
			
		||||
                    cfg[CONF_GATING_MAX_DURATION_MINUTES],
 | 
			
		||||
                    cfg[CONF_GAIN_FACTOR],
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
    cg.add_library(
 | 
			
		||||
        None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git"
 | 
			
		||||
    )
 | 
			
		||||
							
								
								
									
										343
									
								
								esphome/components/sgp4x/sgp4x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								esphome/components/sgp4x/sgp4x.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,343 @@
 | 
			
		||||
#include "sgp4x.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sgp4x {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "sgp4x";
 | 
			
		||||
 | 
			
		||||
void SGP4xComponent::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up SGP4x...");
 | 
			
		||||
 | 
			
		||||
  // Serial Number identification
 | 
			
		||||
  uint16_t raw_serial_number[3];
 | 
			
		||||
  if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to read serial number");
 | 
			
		||||
    this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
 | 
			
		||||
                         (uint64_t(raw_serial_number[2]));
 | 
			
		||||
  ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
 | 
			
		||||
 | 
			
		||||
  // Featureset identification for future use
 | 
			
		||||
  uint16_t raw_featureset;
 | 
			
		||||
  if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) {
 | 
			
		||||
    ESP_LOGD(TAG, "raw_featureset write_command_ failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->featureset_ = raw_featureset;
 | 
			
		||||
  if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) {
 | 
			
		||||
    sgp_type_ = SGP40;
 | 
			
		||||
    self_test_time_ = SPG40_SELFTEST_TIME;
 | 
			
		||||
    measure_time_ = SGP40_MEASURE_TIME;
 | 
			
		||||
    if (this->nox_sensor_) {
 | 
			
		||||
      ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected");
 | 
			
		||||
      // disable the sensor
 | 
			
		||||
      this->nox_sensor_->set_disabled_by_default(true);
 | 
			
		||||
      // make sure it's not visiable in HA
 | 
			
		||||
      this->nox_sensor_->set_internal(true);
 | 
			
		||||
      this->nox_sensor_->state = NAN;
 | 
			
		||||
      // remove pointer to sensor
 | 
			
		||||
      this->nox_sensor_ = nullptr;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) {
 | 
			
		||||
      sgp_type_ = SGP41;
 | 
			
		||||
      self_test_time_ = SPG41_SELFTEST_TIME;
 | 
			
		||||
      measure_time_ = SGP41_MEASURE_TIME;
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
 | 
			
		||||
               SGP40_FEATURESET);
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
 | 
			
		||||
 | 
			
		||||
  if (this->store_baseline_) {
 | 
			
		||||
    // Hash with compilation time
 | 
			
		||||
    // This ensures the baseline storage is cleared after OTA
 | 
			
		||||
    uint32_t hash = fnv1_hash(App.get_compilation_time());
 | 
			
		||||
    this->pref_ = global_preferences->make_preference<SGP4xBaselines>(hash, true);
 | 
			
		||||
 | 
			
		||||
    if (this->pref_.load(&this->voc_baselines_storage_)) {
 | 
			
		||||
      this->voc_state0_ = this->voc_baselines_storage_.state0;
 | 
			
		||||
      this->voc_state1_ = this->voc_baselines_storage_.state1;
 | 
			
		||||
      ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
 | 
			
		||||
               voc_baselines_storage_.state1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize storage timestamp
 | 
			
		||||
    this->seconds_since_last_store_ = 0;
 | 
			
		||||
 | 
			
		||||
    if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
 | 
			
		||||
      ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
 | 
			
		||||
               this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
 | 
			
		||||
      voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
 | 
			
		||||
    voc_algorithm_.set_tuning_parameters(
 | 
			
		||||
        voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
 | 
			
		||||
        voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
 | 
			
		||||
        voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
 | 
			
		||||
    nox_algorithm_.set_tuning_parameters(
 | 
			
		||||
        nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
 | 
			
		||||
        nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
 | 
			
		||||
        nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->self_test_();
 | 
			
		||||
 | 
			
		||||
  /* The official spec for this sensor at
 | 
			
		||||
  https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
 | 
			
		||||
  sensor should be driven at 1Hz. Comments from the developers at:
 | 
			
		||||
  https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
 | 
			
		||||
  timing variations so the software timer should be accurate enough for this.
 | 
			
		||||
 | 
			
		||||
  This block starts sampling from the sensor at 1Hz, and is done seperately from the call
 | 
			
		||||
  to the update method. This seperation is to support getting accurate measurements but
 | 
			
		||||
  limit the amount of communication done over wifi for power consumption or to keep the
 | 
			
		||||
  number of records reported from being overwhelming.
 | 
			
		||||
  */
 | 
			
		||||
  ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
 | 
			
		||||
  this->set_interval(1000, [this]() { this->update_gas_indices(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP4xComponent::self_test_() {
 | 
			
		||||
  ESP_LOGD(TAG, "Self-test started");
 | 
			
		||||
  if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    ESP_LOGD(TAG, "Self-test communication failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->set_timeout(self_test_time_, [this]() {
 | 
			
		||||
    uint16_t reply;
 | 
			
		||||
    if (!this->read_data(reply)) {
 | 
			
		||||
      this->error_code_ = SELF_TEST_FAILED;
 | 
			
		||||
      ESP_LOGD(TAG, "Self-test read_data_ failed");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (reply == 0xD400) {
 | 
			
		||||
      this->self_test_complete_ = true;
 | 
			
		||||
      ESP_LOGD(TAG, "Self-test completed");
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->error_code_ = SELF_TEST_FAILED;
 | 
			
		||||
      ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Combined the measured gasses, temperature, and humidity
 | 
			
		||||
 * to calculate the VOC Index
 | 
			
		||||
 *
 | 
			
		||||
 * @param temperature The measured temperature in degrees C
 | 
			
		||||
 * @param humidity The measured relative humidity in % rH
 | 
			
		||||
 * @return int32_t The VOC Index
 | 
			
		||||
 */
 | 
			
		||||
bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
 | 
			
		||||
  uint16_t voc_sraw;
 | 
			
		||||
  uint16_t nox_sraw;
 | 
			
		||||
  if (!measure_raw_(voc_sraw, nox_sraw))
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
 | 
			
		||||
  voc = voc_algorithm_.process(voc_sraw);
 | 
			
		||||
  if (nox_sensor_) {
 | 
			
		||||
    nox = nox_algorithm_.process(nox_sraw);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox);
 | 
			
		||||
  // Store baselines after defined interval or if the difference between current and stored baseline becomes too
 | 
			
		||||
  // much
 | 
			
		||||
  if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
 | 
			
		||||
    voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
 | 
			
		||||
    if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
 | 
			
		||||
        (uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
 | 
			
		||||
      this->seconds_since_last_store_ = 0;
 | 
			
		||||
      this->voc_baselines_storage_.state0 = this->voc_state0_;
 | 
			
		||||
      this->voc_baselines_storage_.state1 = this->voc_state1_;
 | 
			
		||||
 | 
			
		||||
      if (this->pref_.save(&this->voc_baselines_storage_)) {
 | 
			
		||||
        ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
 | 
			
		||||
                 voc_baselines_storage_.state1);
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Could not store VOC baselines");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Return the raw gas measurement
 | 
			
		||||
 *
 | 
			
		||||
 * @param temperature The measured temperature in degrees C
 | 
			
		||||
 * @param humidity The measured relative humidity in % rH
 | 
			
		||||
 * @return uint16_t The current raw gas measurement
 | 
			
		||||
 */
 | 
			
		||||
bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
 | 
			
		||||
  float humidity = NAN;
 | 
			
		||||
  static uint32_t nox_conditioning_start = millis();
 | 
			
		||||
 | 
			
		||||
  if (!this->self_test_complete_) {
 | 
			
		||||
    ESP_LOGD(TAG, "Self-test not yet complete");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
    humidity = this->humidity_sensor_->state;
 | 
			
		||||
  }
 | 
			
		||||
  if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
 | 
			
		||||
    humidity = 50;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float temperature = NAN;
 | 
			
		||||
  if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
    temperature = float(this->temperature_sensor_->state);
 | 
			
		||||
  }
 | 
			
		||||
  if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
 | 
			
		||||
    temperature = 25;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t command;
 | 
			
		||||
  uint16_t data[2];
 | 
			
		||||
  size_t response_words;
 | 
			
		||||
  // Use SGP40 measure command if we don't care about NOx
 | 
			
		||||
  if (nox_sensor_ == nullptr) {
 | 
			
		||||
    command = SGP40_CMD_MEASURE_RAW;
 | 
			
		||||
    response_words = 1;
 | 
			
		||||
  } else {
 | 
			
		||||
    // SGP41 sensor must use NOx conditioning command for the first 10 seconds
 | 
			
		||||
    if (millis() - nox_conditioning_start < 10000) {
 | 
			
		||||
      command = SGP41_CMD_NOX_CONDITIONING;
 | 
			
		||||
      response_words = 1;
 | 
			
		||||
    } else {
 | 
			
		||||
      command = SGP41_CMD_MEASURE_RAW;
 | 
			
		||||
      response_words = 2;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
 | 
			
		||||
  uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
 | 
			
		||||
  // first paramater are the relative humidity ticks
 | 
			
		||||
  data[0] = rhticks;
 | 
			
		||||
  // secomd paramater are the temperature ticks
 | 
			
		||||
  data[1] = tempticks;
 | 
			
		||||
 | 
			
		||||
  if (!this->write_command(command, data, 2)) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    ESP_LOGD(TAG, "write error (%d)", this->last_error_);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  delay(measure_time_);
 | 
			
		||||
  uint16_t raw_data[2];
 | 
			
		||||
  raw_data[1] = 0;
 | 
			
		||||
  if (!this->read_data(raw_data, response_words)) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    ESP_LOGD(TAG, "read error (%d)", this->last_error_);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  voc_raw = raw_data[0];
 | 
			
		||||
  nox_raw = raw_data[1];  // either 0 or the measured NOx ticks
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP4xComponent::update_gas_indices() {
 | 
			
		||||
  if (!this->self_test_complete_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  this->seconds_since_last_store_ += 1;
 | 
			
		||||
  if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
 | 
			
		||||
    // Set values to UINT16_MAX to indicate failure
 | 
			
		||||
    this->voc_index_ = this->nox_index_ = UINT16_MAX;
 | 
			
		||||
    ESP_LOGE(TAG, "measure gas indices failed");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->samples_read_ < this->samples_to_stabilize_) {
 | 
			
		||||
    this->samples_read_++;
 | 
			
		||||
    ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
 | 
			
		||||
             this->samples_to_stabilize_, this->voc_index_);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP4xComponent::update() {
 | 
			
		||||
  if (this->samples_read_ < this->samples_to_stabilize_) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->voc_sensor_) {
 | 
			
		||||
    if (this->voc_index_ != UINT16_MAX) {
 | 
			
		||||
      this->status_clear_warning();
 | 
			
		||||
      this->voc_sensor_->publish_state(this->voc_index_);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->nox_sensor_) {
 | 
			
		||||
    if (this->nox_index_ != UINT16_MAX) {
 | 
			
		||||
      this->status_clear_warning();
 | 
			
		||||
      this->nox_sensor_->publish_state(this->nox_index_);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SGP4xComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "SGP4x:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  store_baseline: %d", this->store_baseline_);
 | 
			
		||||
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    switch (this->error_code_) {
 | 
			
		||||
      case COMMUNICATION_FAILED:
 | 
			
		||||
        ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
 | 
			
		||||
        break;
 | 
			
		||||
      case SERIAL_NUMBER_IDENTIFICATION_FAILED:
 | 
			
		||||
        ESP_LOGW(TAG, "Get Serial number failed.");
 | 
			
		||||
        break;
 | 
			
		||||
      case SELF_TEST_FAILED:
 | 
			
		||||
        ESP_LOGW(TAG, "Self test failed.");
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        ESP_LOGW(TAG, "Unknown setup error!");
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Serial number: %" PRIu64, this->serial_number_);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Compensation:");
 | 
			
		||||
    LOG_SENSOR("    ", "Temperature Source:", this->temperature_sensor_);
 | 
			
		||||
    LOG_SENSOR("    ", "Humidity Source:", this->humidity_sensor_);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Compensation: No source configured");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_SENSOR("  ", "VOC", this->voc_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "NOx", this->nox_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace sgp4x
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										142
									
								
								esphome/components/sgp4x/sgp4x.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/components/sgp4x/sgp4x.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include <VOCGasIndexAlgorithm.h>
 | 
			
		||||
#include <NOxGasIndexAlgorithm.h>
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sgp4x {
 | 
			
		||||
 | 
			
		||||
struct SGP4xBaselines {
 | 
			
		||||
  int32_t state0;
 | 
			
		||||
  int32_t state1;
 | 
			
		||||
} PACKED;  // NOLINT
 | 
			
		||||
 | 
			
		||||
enum SgpType { SGP40, SGP41 };
 | 
			
		||||
 | 
			
		||||
struct GasTuning {
 | 
			
		||||
  uint16_t index_offset;
 | 
			
		||||
  uint16_t learning_time_offset_hours;
 | 
			
		||||
  uint16_t learning_time_gain_hours;
 | 
			
		||||
  uint16_t gating_max_duration_minutes;
 | 
			
		||||
  uint16_t std_initial;
 | 
			
		||||
  uint16_t gain_factor;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// commands and constants
 | 
			
		||||
static const uint8_t SGP40_FEATURESET = 0x0020;  // can measure VOC
 | 
			
		||||
static const uint8_t SGP41_FEATURESET = 0x0040;  // can measure VOC and NOX
 | 
			
		||||
// Commands
 | 
			
		||||
static const uint16_t SGP4X_CMD_GET_SERIAL_ID = 0x3682;
 | 
			
		||||
static const uint16_t SGP4X_CMD_GET_FEATURESET = 0x202f;
 | 
			
		||||
static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e;
 | 
			
		||||
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
 | 
			
		||||
static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619;
 | 
			
		||||
static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612;
 | 
			
		||||
static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12;
 | 
			
		||||
 | 
			
		||||
// Shortest time interval of 3H for storing baseline values.
 | 
			
		||||
// Prevents wear of the flash because of too many write operations
 | 
			
		||||
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
 | 
			
		||||
static const uint16_t SPG40_SELFTEST_TIME = 250;  // 250 ms for self test
 | 
			
		||||
static const uint16_t SPG41_SELFTEST_TIME = 320;  // 320 ms for self test
 | 
			
		||||
static const uint16_t SGP40_MEASURE_TIME = 30;
 | 
			
		||||
static const uint16_t SGP41_MEASURE_TIME = 55;
 | 
			
		||||
// Store anyway if the baseline difference exceeds the max storage diff value
 | 
			
		||||
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
 | 
			
		||||
 | 
			
		||||
class SGP4xComponent;
 | 
			
		||||
 | 
			
		||||
/// This class implements support for the Sensirion sgp4x i2c GAS (VOC) sensors.
 | 
			
		||||
class SGP4xComponent : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
 | 
			
		||||
  enum ErrorCode {
 | 
			
		||||
    COMMUNICATION_FAILED,
 | 
			
		||||
    MEASUREMENT_INIT_FAILED,
 | 
			
		||||
    INVALID_ID,
 | 
			
		||||
    UNSUPPORTED_ID,
 | 
			
		||||
    SERIAL_NUMBER_IDENTIFICATION_FAILED,
 | 
			
		||||
    SELF_TEST_FAILED,
 | 
			
		||||
    UNKNOWN
 | 
			
		||||
  } error_code_{UNKNOWN};
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  // SGP4xComponent()  {};
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void update_gas_indices();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
 | 
			
		||||
  void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
 | 
			
		||||
  void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
 | 
			
		||||
  void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
 | 
			
		||||
                                uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
 | 
			
		||||
                                uint16_t std_initial, uint16_t gain_factor) {
 | 
			
		||||
    voc_tuning_params_.value().index_offset = index_offset;
 | 
			
		||||
    voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
 | 
			
		||||
    voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
 | 
			
		||||
    voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
 | 
			
		||||
    voc_tuning_params_.value().std_initial = std_initial;
 | 
			
		||||
    voc_tuning_params_.value().gain_factor = gain_factor;
 | 
			
		||||
  }
 | 
			
		||||
  void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
 | 
			
		||||
                                uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
 | 
			
		||||
                                uint16_t gain_factor) {
 | 
			
		||||
    nox_tuning_params_.value().index_offset = index_offset;
 | 
			
		||||
    nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
 | 
			
		||||
    nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
 | 
			
		||||
    nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
 | 
			
		||||
    nox_tuning_params_.value().std_initial = 50;
 | 
			
		||||
    nox_tuning_params_.value().gain_factor = gain_factor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void self_test_();
 | 
			
		||||
 | 
			
		||||
  /// Input sensor for humidity and temperature compensation.
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  int16_t sensirion_init_sensors_();
 | 
			
		||||
 | 
			
		||||
  bool measure_gas_indices_(int32_t &voc, int32_t &nox);
 | 
			
		||||
  bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw);
 | 
			
		||||
 | 
			
		||||
  SgpType sgp_type_{SGP40};
 | 
			
		||||
  uint64_t serial_number_;
 | 
			
		||||
  uint16_t featureset_;
 | 
			
		||||
 | 
			
		||||
  bool self_test_complete_;
 | 
			
		||||
  uint16_t self_test_time_;
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *voc_sensor_{nullptr};
 | 
			
		||||
  VOCGasIndexAlgorithm voc_algorithm_;
 | 
			
		||||
  optional<GasTuning> voc_tuning_params_;
 | 
			
		||||
  int32_t voc_state0_;
 | 
			
		||||
  int32_t voc_state1_;
 | 
			
		||||
  int32_t voc_index_ = 0;
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *nox_sensor_{nullptr};
 | 
			
		||||
  int32_t nox_index_ = 0;
 | 
			
		||||
  NOxGasIndexAlgorithm nox_algorithm_;
 | 
			
		||||
  optional<GasTuning> nox_tuning_params_;
 | 
			
		||||
 | 
			
		||||
  uint16_t measure_time_;
 | 
			
		||||
  uint8_t samples_read_ = 0;
 | 
			
		||||
  uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2;
 | 
			
		||||
 | 
			
		||||
  bool store_baseline_;
 | 
			
		||||
  ESPPreferenceObject pref_;
 | 
			
		||||
  uint32_t seconds_since_last_store_;
 | 
			
		||||
  SGP4xBaselines voc_baselines_storage_;
 | 
			
		||||
};
 | 
			
		||||
}  // namespace sgp4x
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -158,11 +158,8 @@ bool ShellyDimmer::upgrade_firmware_() {
 | 
			
		||||
  ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
 | 
			
		||||
  this->reset_dfu_boot_();
 | 
			
		||||
 | 
			
		||||
  // Could be constexpr in c++17
 | 
			
		||||
  static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
 | 
			
		||||
 | 
			
		||||
  // Cleanup with RAII
 | 
			
		||||
  std::unique_ptr<stm32_t, decltype(CLOSE)> stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE};
 | 
			
		||||
  auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
 | 
			
		||||
 | 
			
		||||
  if (!stm32) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to initialize STM32");
 | 
			
		||||
@@ -170,7 +167,7 @@ bool ShellyDimmer::upgrade_firmware_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Erase STM32 flash.
 | 
			
		||||
  if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
 | 
			
		||||
  if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -196,7 +193,7 @@ bool ShellyDimmer::upgrade_firmware_() {
 | 
			
		||||
    std::memcpy(buffer, p, BUFFER_SIZE);
 | 
			
		||||
    p += BUFFER_SIZE;
 | 
			
		||||
 | 
			
		||||
    if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) {
 | 
			
		||||
    if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@ namespace shelly_dimmer {
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
 | 
			
		||||
int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) {
 | 
			
		||||
  if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
 | 
			
		||||
    return 0;
 | 
			
		||||
 | 
			
		||||
@@ -135,7 +135,7 @@ int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
 | 
			
		||||
  return addr ? page + 1 : page;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
 | 
			
		||||
stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) {
 | 
			
		||||
  auto *stream = stm->stream;
 | 
			
		||||
  uint8_t rxbyte;
 | 
			
		||||
 | 
			
		||||
@@ -168,9 +168,9 @@ stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
 | 
			
		||||
  } while (true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); }
 | 
			
		||||
stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); }
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) {
 | 
			
		||||
stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  static constexpr auto BUFFER_SIZE = 2;
 | 
			
		||||
@@ -194,12 +194,12 @@ stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, co
 | 
			
		||||
  return STM32_ERR_UNKNOWN;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) {
 | 
			
		||||
stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) {
 | 
			
		||||
  return stm32_send_command_timeout(stm, cmd, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* if we have lost sync, send a wrong command and expect a NACK */
 | 
			
		||||
stm32_err_t stm32_resync(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_resync(const stm32_unique_ptr &stm) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
  uint32_t t0 = millis();
 | 
			
		||||
  auto t1 = t0;
 | 
			
		||||
@@ -238,7 +238,7 @@ stm32_err_t stm32_resync(const stm32_t *stm) {
 | 
			
		||||
 *
 | 
			
		||||
 * len is value of the first byte in the frame.
 | 
			
		||||
 */
 | 
			
		||||
stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
 | 
			
		||||
stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
 | 
			
		||||
@@ -286,7 +286,7 @@ stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *
 | 
			
		||||
 * This function sends the init sequence and, in case of timeout, recovers
 | 
			
		||||
 * the interface.
 | 
			
		||||
 */
 | 
			
		||||
stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  stream->write_array(&STM32_CMD_INIT, 1);
 | 
			
		||||
@@ -320,7 +320,7 @@ stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
 | 
			
		||||
  return STM32_ERR_UNKNOWN;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_mass_erase(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
 | 
			
		||||
@@ -364,7 +364,7 @@ template<typename T> std::unique_ptr<T[], void (*)(T *memory)> malloc_array_raii
 | 
			
		||||
                                                 DELETOR};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) {
 | 
			
		||||
stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
  uint8_t cs = 0;
 | 
			
		||||
  int i = 0;
 | 
			
		||||
@@ -474,6 +474,18 @@ template<size_t N> void populate_buffer_with_address(uint8_t (&buffer)[N], uint3
 | 
			
		||||
  buffer[4] = static_cast<uint8_t>(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T> stm32_unique_ptr make_stm32_with_deletor(T ptr) {
 | 
			
		||||
  static const auto CLOSE = [](stm32_t *stm32) {
 | 
			
		||||
    if (stm32) {
 | 
			
		||||
      free(stm32->cmd);  // NOLINT
 | 
			
		||||
    }
 | 
			
		||||
    free(stm32);  // NOLINT
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Cleanup with RAII
 | 
			
		||||
  return std::unique_ptr<stm32_t, decltype(CLOSE)>{ptr, CLOSE};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // Anonymous namespace
 | 
			
		||||
 | 
			
		||||
}  // namespace shelly_dimmer
 | 
			
		||||
@@ -485,48 +497,44 @@ namespace shelly_dimmer {
 | 
			
		||||
/* find newer command by higher code */
 | 
			
		||||
#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a)))
 | 
			
		||||
 | 
			
		||||
stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
 | 
			
		||||
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
 | 
			
		||||
  uint8_t buf[257];
 | 
			
		||||
 | 
			
		||||
  // Could be constexpr in c++17
 | 
			
		||||
  static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
 | 
			
		||||
 | 
			
		||||
  // Cleanup with RAII
 | 
			
		||||
  std::unique_ptr<stm32_t, decltype(CLOSE)> stm{static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1)),  // NOLINT
 | 
			
		||||
                                                CLOSE};
 | 
			
		||||
  auto stm = make_stm32_with_deletor(static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1)));  // NOLINT
 | 
			
		||||
 | 
			
		||||
  if (!stm) {
 | 
			
		||||
    return nullptr;
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
  stm->stream = stream;
 | 
			
		||||
  stm->flags = flags;
 | 
			
		||||
 | 
			
		||||
  stm->cmd = static_cast<stm32_cmd_t *>(malloc(sizeof(stm32_cmd_t)));  // NOLINT
 | 
			
		||||
  if (!stm->cmd) {
 | 
			
		||||
    return nullptr;
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
  memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t));
 | 
			
		||||
 | 
			
		||||
  if ((stm->flags & STREAM_OPT_CMD_INIT) && init) {
 | 
			
		||||
    if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK)
 | 
			
		||||
      return nullptr;  // NOLINT
 | 
			
		||||
    if (stm32_send_init_seq(stm) != STM32_ERR_OK)
 | 
			
		||||
      return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* get the version and read protection status  */
 | 
			
		||||
  if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) {
 | 
			
		||||
    return nullptr;  // NOLINT
 | 
			
		||||
  if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) {
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* From AN, only UART bootloader returns 3 bytes */
 | 
			
		||||
  {
 | 
			
		||||
    const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1;
 | 
			
		||||
    if (!stream->read_array(buf, len))
 | 
			
		||||
      return nullptr;  // NOLINT
 | 
			
		||||
      return make_stm32_with_deletor(nullptr);
 | 
			
		||||
 | 
			
		||||
    stm->version = buf[0];
 | 
			
		||||
    stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0;
 | 
			
		||||
    stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0;
 | 
			
		||||
    if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
 | 
			
		||||
      return nullptr;
 | 
			
		||||
    if (stm32_get_ack(stm) != STM32_ERR_OK) {
 | 
			
		||||
      return make_stm32_with_deletor(nullptr);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -544,8 +552,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
			
		||||
      return STM32_CMD_GET_LENGTH;
 | 
			
		||||
    })();
 | 
			
		||||
 | 
			
		||||
    if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK)
 | 
			
		||||
      return nullptr;
 | 
			
		||||
    if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK)
 | 
			
		||||
      return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const auto stop = buf[0] + 1;
 | 
			
		||||
@@ -607,23 +615,23 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
			
		||||
  }
 | 
			
		||||
  if (new_cmds)
 | 
			
		||||
    ESP_LOGD(TAG, ")");
 | 
			
		||||
  if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  if (stm32_get_ack(stm) != STM32_ERR_OK) {
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) {
 | 
			
		||||
    ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command");
 | 
			
		||||
    return nullptr;
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* get the device ID */
 | 
			
		||||
  if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
  const auto returned = buf[0] + 1;
 | 
			
		||||
  if (returned < 2) {
 | 
			
		||||
    ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned);
 | 
			
		||||
    return nullptr;
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
  stm->pid = (buf[1] << 8) | buf[2];
 | 
			
		||||
  if (returned > 2) {
 | 
			
		||||
@@ -631,8 +639,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
			
		||||
    for (auto i = 2; i <= returned; i++)
 | 
			
		||||
      ESP_LOGD(TAG, " %02x", buf[i]);
 | 
			
		||||
  }
 | 
			
		||||
  if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  if (stm32_get_ack(stm) != STM32_ERR_OK) {
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stm->dev = DEVICES;
 | 
			
		||||
@@ -641,21 +649,14 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
			
		||||
 | 
			
		||||
  if (!stm->dev->id) {
 | 
			
		||||
    ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid);
 | 
			
		||||
    return nullptr;
 | 
			
		||||
    return make_stm32_with_deletor(nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: Would be much better if the unique_ptr was returned from this function
 | 
			
		||||
  // Release ownership of unique_ptr
 | 
			
		||||
  return stm.release();  // NOLINT
 | 
			
		||||
  return stm;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void stm32_close(stm32_t *stm) {
 | 
			
		||||
  if (stm)
 | 
			
		||||
    free(stm->cmd);  // NOLINT
 | 
			
		||||
  free(stm);         // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) {
 | 
			
		||||
stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data,
 | 
			
		||||
                              const unsigned int len) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  if (!len)
 | 
			
		||||
@@ -693,7 +694,8 @@ stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_
 | 
			
		||||
  return STM32_ERR_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) {
 | 
			
		||||
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data,
 | 
			
		||||
                               const unsigned int len) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  if (!len)
 | 
			
		||||
@@ -753,7 +755,7 @@ stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8
 | 
			
		||||
  return STM32_ERR_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) {
 | 
			
		||||
  if (stm->cmd->uw == STM32_CMD_ERR) {
 | 
			
		||||
    ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader.");
 | 
			
		||||
    return STM32_ERR_NO_CMD;
 | 
			
		||||
@@ -766,7 +768,7 @@ stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
 | 
			
		||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) {
 | 
			
		||||
  if (stm->cmd->wp == STM32_CMD_ERR) {
 | 
			
		||||
    ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader.");
 | 
			
		||||
    return STM32_ERR_NO_CMD;
 | 
			
		||||
@@ -779,7 +781,7 @@ stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
 | 
			
		||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) {
 | 
			
		||||
  if (stm->cmd->ur == STM32_CMD_ERR) {
 | 
			
		||||
    ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader.");
 | 
			
		||||
    return STM32_ERR_NO_CMD;
 | 
			
		||||
@@ -792,7 +794,7 @@ stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
 | 
			
		||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) {
 | 
			
		||||
  if (stm->cmd->rp == STM32_CMD_ERR) {
 | 
			
		||||
    ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader.");
 | 
			
		||||
    return STM32_ERR_NO_CMD;
 | 
			
		||||
@@ -805,7 +807,7 @@ stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
 | 
			
		||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) {
 | 
			
		||||
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) {
 | 
			
		||||
  if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES)))
 | 
			
		||||
    return STM32_ERR_OK;
 | 
			
		||||
 | 
			
		||||
@@ -847,7 +849,7 @@ stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t page
 | 
			
		||||
  return STM32_ERR_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code,
 | 
			
		||||
static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code,
 | 
			
		||||
                                      uint32_t code_size) {
 | 
			
		||||
  static constexpr uint32_t BUFFER_SIZE = 256;
 | 
			
		||||
 | 
			
		||||
@@ -893,7 +895,7 @@ static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_addres
 | 
			
		||||
  return stm32_go(stm, target_address);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
 | 
			
		||||
stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) {
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
  if (stm->cmd->go == STM32_CMD_ERR) {
 | 
			
		||||
@@ -916,7 +918,7 @@ stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
 | 
			
		||||
  return STM32_ERR_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_reset_device(const stm32_t *stm) {
 | 
			
		||||
stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) {
 | 
			
		||||
  const auto target_address = stm->dev->ram_start;
 | 
			
		||||
 | 
			
		||||
  if (stm->dev->flags & F_OBLL) {
 | 
			
		||||
@@ -927,7 +929,8 @@ stm32_err_t stm32_reset_device(const stm32_t *stm) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) {
 | 
			
		||||
stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length,
 | 
			
		||||
                             uint32_t *const crc) {
 | 
			
		||||
  static constexpr auto BUFFER_SIZE = 5;
 | 
			
		||||
  auto *const stream = stm->stream;
 | 
			
		||||
 | 
			
		||||
@@ -1022,7 +1025,7 @@ uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) {
 | 
			
		||||
  return crc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) {
 | 
			
		||||
stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) {
 | 
			
		||||
  static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF;
 | 
			
		||||
  static constexpr uint32_t BUFFER_SIZE = 256;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@
 | 
			
		||||
#ifdef USE_SHD_FIRMWARE_DATA
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -108,19 +109,20 @@ struct VarlenCmd {
 | 
			
		||||
  uint8_t length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
 | 
			
		||||
void stm32_close(stm32_t *stm);
 | 
			
		||||
stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len);
 | 
			
		||||
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len);
 | 
			
		||||
stm32_err_t stm32_wunprot_memory(const stm32_t *stm);
 | 
			
		||||
stm32_err_t stm32_wprot_memory(const stm32_t *stm);
 | 
			
		||||
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages);
 | 
			
		||||
stm32_err_t stm32_go(const stm32_t *stm, uint32_t address);
 | 
			
		||||
stm32_err_t stm32_reset_device(const stm32_t *stm);
 | 
			
		||||
stm32_err_t stm32_readprot_memory(const stm32_t *stm);
 | 
			
		||||
stm32_err_t stm32_runprot_memory(const stm32_t *stm);
 | 
			
		||||
stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
			
		||||
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
			
		||||
using stm32_unique_ptr = std::unique_ptr<stm32_t, void (*)(stm32_t *)>;
 | 
			
		||||
 | 
			
		||||
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
 | 
			
		||||
stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len);
 | 
			
		||||
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len);
 | 
			
		||||
stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm);
 | 
			
		||||
stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm);
 | 
			
		||||
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages);
 | 
			
		||||
stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address);
 | 
			
		||||
stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm);
 | 
			
		||||
stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm);
 | 
			
		||||
stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm);
 | 
			
		||||
stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
			
		||||
stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
			
		||||
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len);
 | 
			
		||||
 | 
			
		||||
}  // namespace shelly_dimmer
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,6 @@
 | 
			
		||||
 *  O                         FF FF FF FF FF FF FF FF    - Not used
 | 
			
		||||
 *  M                                                 6C - CRC over bytes 2 to F (Addition)
 | 
			
		||||
\*********************************************************************************************/
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include "sonoff_d1.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -263,7 +262,7 @@ void SonoffD1Output::write_state(light::LightState *state) {
 | 
			
		||||
  state->current_values_as_brightness(&brightness);
 | 
			
		||||
 | 
			
		||||
  // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100)
 | 
			
		||||
  const uint8_t calculated_brightness = std::round(brightness * 100);
 | 
			
		||||
  const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100);
 | 
			
		||||
 | 
			
		||||
  if (calculated_brightness == 0) {
 | 
			
		||||
    // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "speed_fan.h"
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ TCS34725Component = tcs34725_ns.class_(
 | 
			
		||||
 | 
			
		||||
TCS34725IntegrationTime = tcs34725_ns.enum("TCS34725IntegrationTime")
 | 
			
		||||
TCS34725_INTEGRATION_TIMES = {
 | 
			
		||||
    "auto": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_AUTO,
 | 
			
		||||
    "2.4ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS,
 | 
			
		||||
    "24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS,
 | 
			
		||||
    "50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS,
 | 
			
		||||
@@ -88,7 +89,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema,
 | 
			
		||||
            cv.Optional(CONF_ILLUMINANCE): illuminance_schema,
 | 
			
		||||
            cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema,
 | 
			
		||||
            cv.Optional(CONF_INTEGRATION_TIME, default="2.4ms"): cv.enum(
 | 
			
		||||
            cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum(
 | 
			
		||||
                TCS34725_INTEGRATION_TIMES, lower=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True),
 | 
			
		||||
 
 | 
			
		||||
@@ -136,8 +136,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
 | 
			
		||||
  }
 | 
			
		||||
  /* Check for saturation and mark the sample as invalid if true */
 | 
			
		||||
  if (c >= sat) {
 | 
			
		||||
    ESP_LOGW(TAG, "Saturation too high, discarding sample with saturation %.1f and clear %d", sat, c);
 | 
			
		||||
    return;
 | 
			
		||||
    if (this->integration_time_auto_) {
 | 
			
		||||
      ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing");
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(
 | 
			
		||||
          TAG,
 | 
			
		||||
          "Saturation too high, sample with saturation %.1f and clear %d treat values carefully or use grey filter",
 | 
			
		||||
          sat, c);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* AMS RGB sensors have no IR channel, so the IR content must be */
 | 
			
		||||
@@ -149,8 +155,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
 | 
			
		||||
  g2 = g - ir;
 | 
			
		||||
  b2 = b - ir;
 | 
			
		||||
 | 
			
		||||
  // discarding super low values? not recemmonded, and avoided by using auto gain.
 | 
			
		||||
  if (r2 == 0) {
 | 
			
		||||
    return;
 | 
			
		||||
    // legacy code
 | 
			
		||||
    if (!this->integration_time_auto_) {
 | 
			
		||||
      ESP_LOGW(TAG,
 | 
			
		||||
               "No light detected on red channel, switch to auto gain or adjust timing, values will be unreliable");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Lux Calculation (DN40 3.2)
 | 
			
		||||
@@ -189,7 +201,7 @@ void TCS34725Component::update() {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Raw values clear=%x red=%x green=%x blue=%x", raw_c, raw_r, raw_g, raw_b);
 | 
			
		||||
  ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b);
 | 
			
		||||
 | 
			
		||||
  float channel_c;
 | 
			
		||||
  float channel_r;
 | 
			
		||||
@@ -220,20 +232,95 @@ void TCS34725Component::update() {
 | 
			
		||||
    calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->illuminance_sensor_ != nullptr)
 | 
			
		||||
    this->illuminance_sensor_->publish_state(this->illuminance_);
 | 
			
		||||
  // do not publish values if auto gain finding ongoing, and oversaturated
 | 
			
		||||
  // so: publish when:
 | 
			
		||||
  // - not auto mode
 | 
			
		||||
  // - clear not oversaturated
 | 
			
		||||
  // - clear oversaturated but gain and timing cannot go lower
 | 
			
		||||
  if (!this->integration_time_auto_ || raw_c < 65530 || (this->gain_reg_ == 0 && this->integration_time_ < 200)) {
 | 
			
		||||
    if (this->illuminance_sensor_ != nullptr)
 | 
			
		||||
      this->illuminance_sensor_->publish_state(this->illuminance_);
 | 
			
		||||
 | 
			
		||||
  if (this->color_temperature_sensor_ != nullptr)
 | 
			
		||||
    this->color_temperature_sensor_->publish_state(this->color_temperature_);
 | 
			
		||||
    if (this->color_temperature_sensor_ != nullptr)
 | 
			
		||||
      this->color_temperature_sensor_->publish_state(this->color_temperature_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color Temperature=%.1fK",
 | 
			
		||||
  ESP_LOGD(TAG,
 | 
			
		||||
           "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color "
 | 
			
		||||
           "Temperature=%.1fK",
 | 
			
		||||
           channel_r, channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_);
 | 
			
		||||
 | 
			
		||||
  if (this->integration_time_auto_) {
 | 
			
		||||
    // change integration time an gain to achieve maximum resolution an dynamic range
 | 
			
		||||
    // calculate optimal integration time to achieve 70% satuaration
 | 
			
		||||
    float integration_time_ideal;
 | 
			
		||||
    integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_;
 | 
			
		||||
 | 
			
		||||
    uint8_t gain_reg_val_new = this->gain_reg_;
 | 
			
		||||
    // increase gain if less than 20% of white channel used and high integration time
 | 
			
		||||
    // increase only if not already maximum
 | 
			
		||||
    // do not use max gain, as ist will not get better
 | 
			
		||||
    if (this->gain_reg_ < 3) {
 | 
			
		||||
      if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) {
 | 
			
		||||
        gain_reg_val_new = this->gain_reg_ + 1;
 | 
			
		||||
        // update integration time to new situation
 | 
			
		||||
        integration_time_ideal = integration_time_ideal / 4;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // decrease gain, if very high clear values and integration times alreadey low
 | 
			
		||||
    if (this->gain_reg_ > 0) {
 | 
			
		||||
      if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) {
 | 
			
		||||
        gain_reg_val_new = this->gain_reg_ - 1;
 | 
			
		||||
        // update integration time to new situation
 | 
			
		||||
        integration_time_ideal = integration_time_ideal * 4;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // saturate integration times
 | 
			
		||||
    float integration_time_next = integration_time_ideal;
 | 
			
		||||
    if (integration_time_ideal > 2.4f * 256) {
 | 
			
		||||
      integration_time_next = 2.4f * 256;
 | 
			
		||||
    }
 | 
			
		||||
    if (integration_time_ideal < 154) {
 | 
			
		||||
      integration_time_next = 154;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // calculate register value from timing
 | 
			
		||||
    uint8_t regval_atime = (uint8_t)(256.f - integration_time_next / 2.4f);
 | 
			
		||||
    ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d  gain reg: %d",
 | 
			
		||||
             this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_);
 | 
			
		||||
 | 
			
		||||
    if (this->integration_reg_ != regval_atime || gain_reg_val_new != this->gain_reg_) {
 | 
			
		||||
      this->integration_reg_ = regval_atime;
 | 
			
		||||
      this->gain_reg_ = gain_reg_val_new;
 | 
			
		||||
      set_gain((TCS34725Gain) gain_reg_val_new);
 | 
			
		||||
      if (this->write_config_register_(TCS34725_REGISTER_ATIME, this->integration_reg_) != i2c::ERROR_OK ||
 | 
			
		||||
          this->write_config_register_(TCS34725_REGISTER_CONTROL, this->gain_reg_) != i2c::ERROR_OK) {
 | 
			
		||||
        this->mark_failed();
 | 
			
		||||
        ESP_LOGW(TAG, "TCS34725I update timing failed!");
 | 
			
		||||
      } else {
 | 
			
		||||
        this->integration_time_ = integration_time_next;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
}
 | 
			
		||||
void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) {
 | 
			
		||||
  this->integration_reg_ = integration_time;
 | 
			
		||||
  this->integration_time_ = (256.f - integration_time) * 2.4f;
 | 
			
		||||
  // if an integration time is 0x100, this is auto start with 154ms as this gives best starting point
 | 
			
		||||
  TCS34725IntegrationTime my_integration_time_regval;
 | 
			
		||||
 | 
			
		||||
  if (integration_time == TCS34725_INTEGRATION_TIME_AUTO) {
 | 
			
		||||
    this->integration_time_auto_ = true;
 | 
			
		||||
    this->integration_reg_ = TCS34725_INTEGRATION_TIME_154MS;
 | 
			
		||||
    my_integration_time_regval = TCS34725_INTEGRATION_TIME_154MS;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->integration_reg_ = integration_time;
 | 
			
		||||
    my_integration_time_regval = integration_time;
 | 
			
		||||
    this->integration_time_auto_ = false;
 | 
			
		||||
  }
 | 
			
		||||
  this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
 | 
			
		||||
  ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
 | 
			
		||||
}
 | 
			
		||||
void TCS34725Component::set_gain(TCS34725Gain gain) {
 | 
			
		||||
  this->gain_reg_ = gain;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ enum TCS34725IntegrationTime {
 | 
			
		||||
  TCS34725_INTEGRATION_TIME_540MS = 0x1F,
 | 
			
		||||
  TCS34725_INTEGRATION_TIME_600MS = 0x06,
 | 
			
		||||
  TCS34725_INTEGRATION_TIME_614MS = 0x00,
 | 
			
		||||
  TCS34725_INTEGRATION_TIME_AUTO = 0x100,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum TCS34725Gain {
 | 
			
		||||
@@ -77,10 +78,11 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
  float glass_attenuation_{1.0};
 | 
			
		||||
  float illuminance_;
 | 
			
		||||
  float color_temperature_;
 | 
			
		||||
  bool integration_time_auto_{true};
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c);
 | 
			
		||||
  uint8_t integration_reg_{TCS34725_INTEGRATION_TIME_2_4MS};
 | 
			
		||||
  uint16_t integration_reg_;
 | 
			
		||||
  uint8_t gain_reg_{TCS34725_GAIN_1X};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,11 +13,11 @@ static const char *const TAG = "time";
 | 
			
		||||
 | 
			
		||||
RealTimeClock::RealTimeClock() = default;
 | 
			
		||||
void RealTimeClock::call_setup() {
 | 
			
		||||
  setenv("TZ", this->timezone_.c_str(), 1);
 | 
			
		||||
  tzset();
 | 
			
		||||
  this->apply_timezone_();
 | 
			
		||||
  PollingComponent::call_setup();
 | 
			
		||||
}
 | 
			
		||||
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
 | 
			
		||||
  // Update UTC epoch time.
 | 
			
		||||
  struct timeval timev {
 | 
			
		||||
    .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
 | 
			
		||||
  };
 | 
			
		||||
@@ -30,6 +30,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
 | 
			
		||||
    ret = settimeofday(&timev, nullptr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Move timezone back to local timezone.
 | 
			
		||||
  this->apply_timezone_();
 | 
			
		||||
 | 
			
		||||
  if (ret != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
 | 
			
		||||
  }
 | 
			
		||||
@@ -41,6 +44,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
 | 
			
		||||
  this->time_sync_callback_.call();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RealTimeClock::apply_timezone_() {
 | 
			
		||||
  setenv("TZ", this->timezone_.c_str(), 1);
 | 
			
		||||
  tzset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
 | 
			
		||||
  struct tm c_tm = this->to_c_tm();
 | 
			
		||||
  return ::strftime(buffer, buffer_len, format, &c_tm);
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,7 @@ class RealTimeClock : public PollingComponent {
 | 
			
		||||
  void synchronize_epoch_(uint32_t epoch);
 | 
			
		||||
 | 
			
		||||
  std::string timezone_{};
 | 
			
		||||
  void apply_timezone_();
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> time_sync_callback_;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from esphome.components import time
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
@@ -11,6 +12,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints"
 | 
			
		||||
 | 
			
		||||
CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update"
 | 
			
		||||
CONF_DATAPOINT_TYPE = "datapoint_type"
 | 
			
		||||
CONF_STATUS_PIN = "status_pin"
 | 
			
		||||
 | 
			
		||||
tuya_ns = cg.esphome_ns.namespace("tuya")
 | 
			
		||||
Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice)
 | 
			
		||||
@@ -88,6 +90,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(
 | 
			
		||||
                cv.uint8_t
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
@@ -114,6 +117,9 @@ async def to_code(config):
 | 
			
		||||
    if CONF_TIME_ID in config:
 | 
			
		||||
        time_ = await cg.get_variable(config[CONF_TIME_ID])
 | 
			
		||||
        cg.add(var.set_time_id(time_))
 | 
			
		||||
    if CONF_STATUS_PIN in config:
 | 
			
		||||
        status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN])
 | 
			
		||||
        cg.add(var.set_status_pin(status_pin_))
 | 
			
		||||
    if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config:
 | 
			
		||||
        for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]:
 | 
			
		||||
            cg.add(var.add_ignore_mcu_update_on_datapoints(dp))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#include "tuya_fan.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								esphome/components/tuya/select/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/tuya/select/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
from esphome.components import select
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.const import CONF_OPTIONS, CONF_OPTIMISTIC, CONF_ENUM_DATAPOINT
 | 
			
		||||
from .. import tuya_ns, CONF_TUYA_ID, Tuya
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["tuya"]
 | 
			
		||||
CODEOWNERS = ["@bearpawmaxim"]
 | 
			
		||||
 | 
			
		||||
TuyaSelect = tuya_ns.class_("TuyaSelect", select.Select, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def ensure_option_map(value):
 | 
			
		||||
    cv.check_not_templatable(value)
 | 
			
		||||
    option = cv.All(cv.int_range(0, 2**8 - 1))
 | 
			
		||||
    mapping = cv.All(cv.string_strict)
 | 
			
		||||
    options_map_schema = cv.Schema({option: mapping})
 | 
			
		||||
    value = options_map_schema(value)
 | 
			
		||||
 | 
			
		||||
    all_values = list(value.keys())
 | 
			
		||||
    unique_values = set(value.keys())
 | 
			
		||||
    if len(all_values) != len(unique_values):
 | 
			
		||||
        raise cv.Invalid("Mapping values must be unique.")
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = select.SELECT_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(TuyaSelect),
 | 
			
		||||
        cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
 | 
			
		||||
        cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t,
 | 
			
		||||
        cv.Required(CONF_OPTIONS): ensure_option_map,
 | 
			
		||||
        cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    options_map = config[CONF_OPTIONS]
 | 
			
		||||
    var = await select.new_select(config, options=list(options_map.values()))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    cg.add(var.set_select_mappings(list(options_map.keys())))
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_TUYA_ID])
 | 
			
		||||
    cg.add(var.set_tuya_parent(parent))
 | 
			
		||||
    cg.add(var.set_select_id(config[CONF_ENUM_DATAPOINT]))
 | 
			
		||||
    cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
 | 
			
		||||
							
								
								
									
										52
									
								
								esphome/components/tuya/select/tuya_select.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/tuya/select/tuya_select.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "tuya_select.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace tuya {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "tuya.select";
 | 
			
		||||
 | 
			
		||||
void TuyaSelect::setup() {
 | 
			
		||||
  this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) {
 | 
			
		||||
    uint8_t enum_value = datapoint.value_enum;
 | 
			
		||||
    ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value);
 | 
			
		||||
    auto options = this->traits.get_options();
 | 
			
		||||
    auto mappings = this->mappings_;
 | 
			
		||||
    auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value);
 | 
			
		||||
    if (it == mappings.end()) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid value %u", enum_value);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    size_t mapping_idx = std::distance(mappings.cbegin(), it);
 | 
			
		||||
    auto value = this->at(mapping_idx);
 | 
			
		||||
    this->publish_state(value.value());
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TuyaSelect::control(const std::string &value) {
 | 
			
		||||
  if (this->optimistic_)
 | 
			
		||||
    this->publish_state(value);
 | 
			
		||||
 | 
			
		||||
  auto idx = this->index_of(value);
 | 
			
		||||
  if (idx.has_value()) {
 | 
			
		||||
    uint8_t mapping = this->mappings_.at(idx.value());
 | 
			
		||||
    ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str());
 | 
			
		||||
    this->parent_->set_enum_datapoint_value(this->select_id_, mapping);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGW(TAG, "Invalid value %s", value.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TuyaSelect::dump_config() {
 | 
			
		||||
  LOG_SELECT("", "Tuya Select", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Select has datapoint ID %u", this->select_id_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Options are:");
 | 
			
		||||
  auto options = this->traits.get_options();
 | 
			
		||||
  for (auto i = 0; i < this->mappings_.size(); i++) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    %i: %s", this->mappings_.at(i), options.at(i).c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace tuya
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										30
									
								
								esphome/components/tuya/select/tuya_select.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/tuya/select/tuya_select.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/tuya/tuya.h"
 | 
			
		||||
#include "esphome/components/select/select.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace tuya {
 | 
			
		||||
 | 
			
		||||
class TuyaSelect : public select::Select, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
 | 
			
		||||
  void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
 | 
			
		||||
  void set_select_id(uint8_t select_id) { this->select_id_ = select_id; }
 | 
			
		||||
  void set_select_mappings(std::vector<uint8_t> mappings) { this->mappings_ = std::move(mappings); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
 | 
			
		||||
  Tuya *parent_;
 | 
			
		||||
  bool optimistic_ = false;
 | 
			
		||||
  uint8_t select_id_;
 | 
			
		||||
  std::vector<uint8_t> mappings_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace tuya
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -20,7 +20,7 @@ void TuyaTextSensor::setup() {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
        ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, datapoint.type);
 | 
			
		||||
        ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type);
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/gpio.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace tuya {
 | 
			
		||||
@@ -10,9 +11,13 @@ namespace tuya {
 | 
			
		||||
static const char *const TAG = "tuya";
 | 
			
		||||
static const int COMMAND_DELAY = 10;
 | 
			
		||||
static const int RECEIVE_TIMEOUT = 300;
 | 
			
		||||
static const int MAX_RETRIES = 5;
 | 
			
		||||
 | 
			
		||||
void Tuya::setup() {
 | 
			
		||||
  this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
 | 
			
		||||
  if (this->status_pin_.has_value()) {
 | 
			
		||||
    this->status_pin_.value()->digital_write(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Tuya::loop() {
 | 
			
		||||
@@ -27,8 +32,12 @@ void Tuya::loop() {
 | 
			
		||||
void Tuya::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Tuya:");
 | 
			
		||||
  if (this->init_state_ != TuyaInitState::INIT_DONE) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Configuration will be reported when setup is complete. Current init_state: %u",
 | 
			
		||||
                  static_cast<uint8_t>(this->init_state_));
 | 
			
		||||
    if (this->init_failed_) {
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Initialization failed. Current init_state: %u", static_cast<uint8_t>(this->init_state_));
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Configuration will be reported when setup is complete. Current init_state: %u",
 | 
			
		||||
                    static_cast<uint8_t>(this->init_state_));
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  If no further output is received, confirm that this is a supported Tuya device.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -49,9 +58,12 @@ void Tuya::dump_config() {
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Datapoint %u: unknown", info.id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_,
 | 
			
		||||
                  this->gpio_reset_);
 | 
			
		||||
  if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_,
 | 
			
		||||
                  this->reset_pin_reported_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->status_pin_.has_value()) {
 | 
			
		||||
    LOG_PIN("  Status Pin: ", this->status_pin_.value());
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Product: '%s'", this->product_.c_str());
 | 
			
		||||
  this->check_uart_settings(9600);
 | 
			
		||||
@@ -127,6 +139,8 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
 | 
			
		||||
 | 
			
		||||
  if (this->expected_response_.has_value() && this->expected_response_ == command_type) {
 | 
			
		||||
    this->expected_response_.reset();
 | 
			
		||||
    this->command_queue_.erase(command_queue_.begin());
 | 
			
		||||
    this->init_retries_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (command_type) {
 | 
			
		||||
@@ -164,16 +178,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
 | 
			
		||||
    }
 | 
			
		||||
    case TuyaCommandType::CONF_QUERY: {
 | 
			
		||||
      if (len >= 2) {
 | 
			
		||||
        this->gpio_status_ = buffer[0];
 | 
			
		||||
        this->gpio_reset_ = buffer[1];
 | 
			
		||||
        this->status_pin_reported_ = buffer[0];
 | 
			
		||||
        this->reset_pin_reported_ = buffer[1];
 | 
			
		||||
      }
 | 
			
		||||
      if (this->init_state_ == TuyaInitState::INIT_CONF) {
 | 
			
		||||
        // If mcu returned status gpio, then we can omit sending wifi state
 | 
			
		||||
        if (this->gpio_status_ != -1) {
 | 
			
		||||
        if (this->status_pin_reported_ != -1) {
 | 
			
		||||
          this->init_state_ = TuyaInitState::INIT_DATAPOINT;
 | 
			
		||||
          this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
 | 
			
		||||
          bool is_pin_equals =
 | 
			
		||||
              this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_;
 | 
			
		||||
          // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send
 | 
			
		||||
          if (is_pin_equals) {
 | 
			
		||||
            ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_);
 | 
			
		||||
            this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); });
 | 
			
		||||
          } else {
 | 
			
		||||
            ESP_LOGW(TAG, "Supplied status_pin does not equals the reported pin %i. TuyaMcu will work in limited mode.",
 | 
			
		||||
                     this->status_pin_reported_);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          this->init_state_ = TuyaInitState::INIT_WIFI;
 | 
			
		||||
          ESP_LOGV(TAG, "Configured WIFI_STATE periodic send");
 | 
			
		||||
          this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -378,13 +403,24 @@ void Tuya::process_command_queue_() {
 | 
			
		||||
 | 
			
		||||
  if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) {
 | 
			
		||||
    this->expected_response_.reset();
 | 
			
		||||
    if (init_state_ != TuyaInitState::INIT_DONE) {
 | 
			
		||||
      if (++this->init_retries_ >= MAX_RETRIES) {
 | 
			
		||||
        this->init_failed_ = true;
 | 
			
		||||
        ESP_LOGE(TAG, "Initialization failed at init_state %u", static_cast<uint8_t>(this->init_state_));
 | 
			
		||||
        this->command_queue_.erase(command_queue_.begin());
 | 
			
		||||
        this->init_retries_ = 0;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      this->command_queue_.erase(command_queue_.begin());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly
 | 
			
		||||
  if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() &&
 | 
			
		||||
      !this->expected_response_.has_value()) {
 | 
			
		||||
    this->send_raw_command_(command_queue_.front());
 | 
			
		||||
    this->command_queue_.erase(command_queue_.begin());
 | 
			
		||||
    if (!this->expected_response_.has_value())
 | 
			
		||||
      this->command_queue_.erase(command_queue_.begin());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -397,16 +433,19 @@ void Tuya::send_empty_command_(TuyaCommandType command) {
 | 
			
		||||
  send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{}});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Tuya::set_status_pin_() {
 | 
			
		||||
  bool is_network_ready = network::is_connected() && remote_is_connected();
 | 
			
		||||
  this->status_pin_.value()->digital_write(is_network_ready);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Tuya::send_wifi_status_() {
 | 
			
		||||
  uint8_t status = 0x02;
 | 
			
		||||
  if (network::is_connected()) {
 | 
			
		||||
    status = 0x03;
 | 
			
		||||
 | 
			
		||||
    // Protocol version 3 also supports specifying when connected to "the cloud"
 | 
			
		||||
    if (this->protocol_version_ >= 0x03) {
 | 
			
		||||
      if (remote_is_connected()) {
 | 
			
		||||
        status = 0x04;
 | 
			
		||||
      }
 | 
			
		||||
    if (this->protocol_version_ >= 0x03 && remote_is_connected()) {
 | 
			
		||||
      status = 0x04;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,7 @@ class Tuya : public Component, public uart::UARTDevice {
 | 
			
		||||
  void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value);
 | 
			
		||||
  void set_boolean_datapoint_value(uint8_t datapoint_id, bool value);
 | 
			
		||||
  void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value);
 | 
			
		||||
  void set_status_pin(InternalGPIOPin *status_pin) { this->status_pin_ = status_pin; }
 | 
			
		||||
  void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value);
 | 
			
		||||
  void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value);
 | 
			
		||||
  void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length);
 | 
			
		||||
@@ -115,6 +116,7 @@ class Tuya : public Component, public uart::UARTDevice {
 | 
			
		||||
  void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced);
 | 
			
		||||
  void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector<uint8_t> &value, bool forced);
 | 
			
		||||
  void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data);
 | 
			
		||||
  void set_status_pin_();
 | 
			
		||||
  void send_wifi_status_();
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
@@ -122,9 +124,12 @@ class Tuya : public Component, public uart::UARTDevice {
 | 
			
		||||
  optional<time::RealTimeClock *> time_id_{};
 | 
			
		||||
#endif
 | 
			
		||||
  TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
 | 
			
		||||
  bool init_failed_{false};
 | 
			
		||||
  int init_retries_{0};
 | 
			
		||||
  uint8_t protocol_version_ = -1;
 | 
			
		||||
  int gpio_status_ = -1;
 | 
			
		||||
  int gpio_reset_ = -1;
 | 
			
		||||
  optional<InternalGPIOPin *> status_pin_{};
 | 
			
		||||
  int status_pin_reported_ = -1;
 | 
			
		||||
  int reset_pin_reported_ = -1;
 | 
			
		||||
  uint32_t last_command_timestamp_ = 0;
 | 
			
		||||
  uint32_t last_rx_char_timestamp_ = 0;
 | 
			
		||||
  std::string product_ = "";
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,6 @@
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#endif
 | 
			
		||||
@@ -482,22 +478,6 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
 | 
			
		||||
    if (traits.supports_speed()) {
 | 
			
		||||
      root["speed_level"] = obj->speed;
 | 
			
		||||
      root["speed_count"] = traits.supported_speed_count();
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
      // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
      switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) {
 | 
			
		||||
        case fan::FAN_SPEED_LOW:  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
          root["speed"] = "low";
 | 
			
		||||
          break;
 | 
			
		||||
        case fan::FAN_SPEED_MEDIUM:  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
          root["speed"] = "medium";
 | 
			
		||||
          break;
 | 
			
		||||
        case fan::FAN_SPEED_HIGH:  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
          root["speed"] = "high";
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
    }
 | 
			
		||||
    if (obj->get_traits().supports_oscillation())
 | 
			
		||||
      root["oscillation"] = obj->oscillating;
 | 
			
		||||
@@ -518,10 +498,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
 | 
			
		||||
      auto call = obj->turn_on();
 | 
			
		||||
      if (request->hasParam("speed")) {
 | 
			
		||||
        String speed = request->getParam("speed")->value();
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
        call.set_speed(speed.c_str());  // NOLINT(clang-diagnostic-deprecated-declarations)
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
      }
 | 
			
		||||
      if (request->hasParam("speed_level")) {
 | 
			
		||||
        String speed_level = request->getParam("speed_level")->value();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
"""Constants used by esphome."""
 | 
			
		||||
 | 
			
		||||
__version__ = "2022.5.0b1"
 | 
			
		||||
__version__ = "2022.6.0-dev"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
 | 
			
		||||
@@ -105,6 +105,7 @@ CONF_COLOR_BRIGHTNESS = "color_brightness"
 | 
			
		||||
CONF_COLOR_CORRECT = "color_correct"
 | 
			
		||||
CONF_COLOR_INTERLOCK = "color_interlock"
 | 
			
		||||
CONF_COLOR_MODE = "color_mode"
 | 
			
		||||
CONF_COLOR_PALETTE = "color_palette"
 | 
			
		||||
CONF_COLOR_TEMPERATURE = "color_temperature"
 | 
			
		||||
CONF_COLORS = "colors"
 | 
			
		||||
CONF_COMMAND = "command"
 | 
			
		||||
@@ -198,6 +199,7 @@ CONF_ENABLE_TIME = "enable_time"
 | 
			
		||||
CONF_ENERGY = "energy"
 | 
			
		||||
CONF_ENTITY_CATEGORY = "entity_category"
 | 
			
		||||
CONF_ENTITY_ID = "entity_id"
 | 
			
		||||
CONF_ENUM_DATAPOINT = "enum_datapoint"
 | 
			
		||||
CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
 | 
			
		||||
CONF_ESPHOME = "esphome"
 | 
			
		||||
CONF_ETHERNET = "ethernet"
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,8 @@ lib_deps =
 | 
			
		||||
    bblanchon/ArduinoJson@6.18.5           ; json
 | 
			
		||||
    wjtje/qr-code-generator-library@1.7.0  ; qr_code
 | 
			
		||||
    functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
 | 
			
		||||
    ; This is using the repository until a new release is published to PlatformIO
 | 
			
		||||
    https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library
 | 
			
		||||
build_flags =
 | 
			
		||||
    -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
 | 
			
		||||
src_filter =
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ tzlocal==4.2    # from time
 | 
			
		||||
tzdata>=2021.1  # from time
 | 
			
		||||
pyserial==3.5
 | 
			
		||||
platformio==5.2.5  # When updating platformio, also update Dockerfile
 | 
			
		||||
esptool==3.3
 | 
			
		||||
esptool==3.3.1
 | 
			
		||||
click==8.1.3
 | 
			
		||||
esphome-dashboard==20220508.0
 | 
			
		||||
aioesphomeapi==10.8.2
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
pylint==2.13.8
 | 
			
		||||
pylint==2.13.9
 | 
			
		||||
flake8==4.0.1
 | 
			
		||||
black==22.3.0
 | 
			
		||||
pyupgrade==2.32.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1901,14 +1901,6 @@ script:
 | 
			
		||||
          preset: SLEEP
 | 
			
		||||
 | 
			
		||||
switch:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: MIDEA_AC_TOGGLE_LIGHT
 | 
			
		||||
    turn_on_action:
 | 
			
		||||
      midea_ac.display_toggle:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: MIDEA_AC_SWING_STEP
 | 
			
		||||
    turn_on_action:
 | 
			
		||||
      midea_ac.swing_step:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: MIDEA_AC_BEEPER_CONTROL
 | 
			
		||||
    optimistic: true
 | 
			
		||||
@@ -2834,3 +2826,23 @@ button:
 | 
			
		||||
          id: scd40
 | 
			
		||||
      - scd4x.factory_reset:
 | 
			
		||||
          id: scd40
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: Midea Display Toggle
 | 
			
		||||
    on_press:
 | 
			
		||||
      midea_ac.display_toggle:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: Midea Swing Step
 | 
			
		||||
    on_press:
 | 
			
		||||
      midea_ac.swing_step:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: Midea Power On
 | 
			
		||||
    on_press:
 | 
			
		||||
      midea_ac.power_on:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: Midea Power Off
 | 
			
		||||
    on_press:
 | 
			
		||||
      midea_ac.power_off:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: Midea Power Inverse
 | 
			
		||||
    on_press:
 | 
			
		||||
      midea_ac.power_toggle:
 | 
			
		||||
 
 | 
			
		||||
@@ -281,10 +281,27 @@ sensor:
 | 
			
		||||
    window_correction_factor: 1.0
 | 
			
		||||
    address: 0x53
 | 
			
		||||
    update_interval: 60s
 | 
			
		||||
  - platform: sgp40
 | 
			
		||||
    name: 'Workshop VOC'
 | 
			
		||||
  - platform: sgp4x
 | 
			
		||||
    voc:
 | 
			
		||||
      name: "VOC Index"
 | 
			
		||||
      id: sgp40_voc_index
 | 
			
		||||
      algorithm_tuning:
 | 
			
		||||
        index_offset: 100
 | 
			
		||||
        learning_time_offset_hours: 12
 | 
			
		||||
        learning_time_gain_hours: 12
 | 
			
		||||
        gating_max_duration_minutes: 180
 | 
			
		||||
        std_initial: 50
 | 
			
		||||
        gain_factor: 230
 | 
			
		||||
    nox:
 | 
			
		||||
      name: "NOx"
 | 
			
		||||
      algorithm_tuning:
 | 
			
		||||
        index_offset: 100
 | 
			
		||||
        learning_time_offset_hours: 12
 | 
			
		||||
        learning_time_gain_hours: 12
 | 
			
		||||
        gating_max_duration_minutes: 180
 | 
			
		||||
        std_initial: 50
 | 
			
		||||
        gain_factor: 230
 | 
			
		||||
    update_interval: 5s
 | 
			
		||||
    store_baseline: 'true'
 | 
			
		||||
  - platform: mcp3008
 | 
			
		||||
    update_interval: 5s
 | 
			
		||||
    mcp3008_id: 'mcp3008_hub'
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,18 @@ time:
 | 
			
		||||
 | 
			
		||||
tuya:
 | 
			
		||||
  time_id: sntp_time
 | 
			
		||||
  status_pin:
 | 
			
		||||
    number: 14
 | 
			
		||||
    inverted: true
 | 
			
		||||
 | 
			
		||||
select:
 | 
			
		||||
  - platform: tuya
 | 
			
		||||
    id: tuya_select
 | 
			
		||||
    enum_datapoint: 42
 | 
			
		||||
    options:
 | 
			
		||||
      0: Internal
 | 
			
		||||
      1: Floor
 | 
			
		||||
      2: Both
 | 
			
		||||
 | 
			
		||||
pipsolar:
 | 
			
		||||
    id: inverter0
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user