diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 52ac3648b0..864586fe6b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,3 @@ # These are supported funding model platforms -github: -patreon: ottowinter -open_collective: -ko_fi: -tidelift: -custom: https://esphome.io/guides/supporters.html +custom: https://www.nabucasa.com diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed4343202c..d2230b3da7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,7 @@ jobs: uses: actions/cache@v1 with: path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - name: Set up environment diff --git a/.github/workflows/docker-lint-build.yml b/.github/workflows/docker-lint-build.yml new file mode 100644 index 0000000000..f148d98d65 --- /dev/null +++ b/.github/workflows/docker-lint-build.yml @@ -0,0 +1,36 @@ +name: Build and publish lint docker image + +# Only run when docker paths change +on: + push: + branches: [dev] + paths: + - 'docker/Dockerfile.lint' + - 'requirements.txt' + - 'requirements_test.txt' + - 'platformio.ini' + - '.github/workflows/docker-lint-build.yml' + +jobs: + publish-docker-lint-iage: + name: Build docker containers + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Pull for cache + run: | + docker pull "esphome/esphome-lint:latest" || true + - name: Build + run: | + docker build \ + --cache-from "esphome/esphome-lint:latest" \ + --file "docker/Dockerfile.lint" \ + --tag "esphome/esphome-lint:latest" \ + . + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - run: | + docker push "esphome/esphome-lint:latest" diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index b4c4a8f17d..f0dc4bd0c0 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -112,7 +112,7 @@ jobs: uses: actions/cache@v1 with: path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - name: Set up environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1fd2dba24..1eca3be269 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,7 +111,7 @@ jobs: uses: actions/cache@v1 with: path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - name: Set up environment diff --git a/CODEOWNERS b/CODEOWNERS index 0a1f2b3ed2..2269730017 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,8 +18,11 @@ esphome/components/animation/* @syndlex esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl +esphome/components/b_parasite/* @rbaron esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/ble_client/* @buxtronix +esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core @@ -34,8 +37,10 @@ esphome/components/ds1307/* @badbadc0ffee esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter +esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core +esphome/components/gps/* @coogle esphome/components/homeassistant/* @OttoWinter esphome/components/i2c/* @esphome/core esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -76,8 +81,11 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sensor/* @esphome/core +esphome/components/sgp40/* @SenexCrenshaw +esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet +esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/spi/* @esphome/core esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 @@ -95,12 +103,14 @@ esphome/components/st7789v/* @kbx81 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core +esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage +esphome/components/tof10120/* @wstrzalka esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/sensor/* @jesserockz diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c5be227278..b91a3b4f83 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@otto-winter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esphome@nabucasa.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/esphome/__main__.py b/esphome/__main__.py index 79a8c708a4..1ec72d9255 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -599,10 +599,10 @@ def run_esphome(argv): _LOGGER.error("Missing configuration parameter, see esphome --help.") return 1 - if sys.version_info < (3, 6, 0): + if sys.version_info < (3, 7, 0): _LOGGER.error( - "You're running ESPHome with Python <3.6. ESPHome is no longer compatible " - "with this Python version. Please reinstall ESPHome with Python 3.6+" + "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.7+" ) return 1 diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index b07db9ed7c..8532f349e6 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -8,8 +8,8 @@ CODEOWNERS = ["@OttoWinter"] @coroutine_with_priority(200.0) def to_code(config): if CORE.is_esp32: - # https://github.com/OttoWinter/AsyncTCP/blob/master/library.json - cg.add_library("AsyncTCP-esphome", "1.1.1") + # https://github.com/esphome/AsyncTCP/blob/master/library.json + cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP cg.add_library("ESPAsyncTCP-esphome", "1.2.3") diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 85e38fce3e..d732212cdd 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -58,6 +58,24 @@ void ATM90E32Component::update() { if (this->phase_[2].power_factor_sensor_ != nullptr) { this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_()); } + if (this->phase_[0].forward_active_energy_sensor_ != nullptr) { + this->phase_[0].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_a_()); + } + if (this->phase_[1].forward_active_energy_sensor_ != nullptr) { + this->phase_[1].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_b_()); + } + if (this->phase_[2].forward_active_energy_sensor_ != nullptr) { + this->phase_[2].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_c_()); + } + if (this->phase_[0].reverse_active_energy_sensor_ != nullptr) { + this->phase_[0].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_a_()); + } + if (this->phase_[1].reverse_active_energy_sensor_ != nullptr) { + this->phase_[1].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_b_()); + } + if (this->phase_[2].reverse_active_energy_sensor_ != nullptr) { + this->phase_[2].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_c_()); + } if (this->freq_sensor_ != nullptr) { this->freq_sensor_->publish_state(this->get_frequency_()); } @@ -119,16 +137,22 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[0].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[0].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[1].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[1].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[2].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[2].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } @@ -239,6 +263,30 @@ float ATM90E32Component::get_power_factor_c_() { int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC); return (float) pf / 1000; } +float ATM90E32Component::get_forward_active_energy_a_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA); + return (float) val * 10 / 3200; // convert register value to WattHours +} +float ATM90E32Component::get_forward_active_energy_b_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_forward_active_energy_c_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_reverse_active_energy_a_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_reverse_active_energy_b_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_reverse_active_energy_c_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC); + return (float) val * 10 / 3200; +} float ATM90E32Component::get_frequency_() { uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); return (float) freq / 100; diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index eb5de3878c..89d62adaf6 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -20,6 +20,12 @@ class ATM90E32Component : public PollingComponent, void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; } + void set_forward_active_energy_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].forward_active_energy_sensor_ = obj; + } + void set_reverse_active_energy_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].reverse_active_energy_sensor_ = obj; + } void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } @@ -52,6 +58,12 @@ class ATM90E32Component : public PollingComponent, float get_power_factor_a_(); float get_power_factor_b_(); float get_power_factor_c_(); + float get_forward_active_energy_a_(); + float get_forward_active_energy_b_(); + float get_forward_active_energy_c_(); + float get_reverse_active_energy_a_(); + float get_reverse_active_energy_b_(); + float get_reverse_active_energy_c_(); float get_frequency_(); float get_chip_temperature_(); @@ -63,6 +75,8 @@ class ATM90E32Component : public PollingComponent, sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *reactive_power_sensor_{nullptr}; sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *forward_active_energy_sensor_{nullptr}; + sensor::Sensor *reverse_active_energy_sensor_{nullptr}; } phase_[3]; sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr}; diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index d0813cfa52..4a9100d9d6 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -8,8 +8,11 @@ from esphome.const import ( CONF_POWER, CONF_POWER_FACTOR, CONF_FREQUENCY, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, @@ -24,6 +27,7 @@ from esphome.const import ( UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT_HOURS, ) CONF_PHASE_A = "phase_a" @@ -73,6 +77,12 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_POWER_FACTOR ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( + UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( + UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, } @@ -129,6 +139,12 @@ def to_code(config): if CONF_POWER_FACTOR in conf: sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR]) cg.add(var.set_power_factor_sensor(i, sens)) + if CONF_FORWARD_ACTIVE_ENERGY in conf: + sens = yield sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY]) + cg.add(var.set_forward_active_energy_sensor(i, sens)) + if CONF_REVERSE_ACTIVE_ENERGY in conf: + sens = yield sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY]) + cg.add(var.set_reverse_active_energy_sensor(i, sens)) if CONF_FREQUENCY in config: sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_freq_sensor(sens)) diff --git a/esphome/components/b_parasite/__init__.py b/esphome/components/b_parasite/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp new file mode 100644 index 0000000000..828e392993 --- /dev/null +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -0,0 +1,82 @@ +#include "b_parasite.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace b_parasite { + +static const char* TAG = "b_parasite"; + +void BParasite::dump_config() { + ESP_LOGCONFIG(TAG, "b_parasite"); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Soil Moisture", this->soil_moisture_); +} + +bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice& device) { + if (device.address_uint64() != address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + const auto& service_datas = device.get_service_datas(); + if (service_datas.size() != 1) { + ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size()); + return false; + } + const auto& service_data = service_datas[0]; + + ESP_LOGVV(TAG, "Service data:"); + for (const uint8_t byte : service_data.data) { + ESP_LOGVV(TAG, "0x%02x", byte); + } + + const auto& data = service_data.data; + + // Counter for deduplicating messages. + uint8_t counter = data[1] & 0x0f; + if (last_processed_counter_ == counter) { + ESP_LOGVV(TAG, "Skipping already processed counter (%u)", counter); + return false; + } + + // Battery voltage in millivolts. + uint16_t battery_millivolt = data[2] << 8 | data[3]; + float battery_voltage = battery_millivolt / 1000.0f; + + // Temperature in 1000 * Celcius. + uint16_t temp_millicelcius = data[4] << 8 | data[5]; + float temp_celcius = temp_millicelcius / 1000.0f; + + // Relative air humidity in the range [0, 2^16). + uint16_t humidity = data[6] << 8 | data[7]; + float humidity_percent = (100.0f * humidity) / (1 << 16); + + // Relative soil moisture in [0 - 2^16). + uint16_t soil_moisture = data[8] << 8 | data[9]; + float moisture_percent = (100.0f * soil_moisture) / (1 << 16); + + if (battery_voltage_ != nullptr) { + battery_voltage_->publish_state(battery_voltage); + } + if (temperature_ != nullptr) { + temperature_->publish_state(temp_celcius); + } + if (humidity_ != nullptr) { + humidity_->publish_state(humidity_percent); + } + if (soil_moisture_ != nullptr) { + soil_moisture_->publish_state(moisture_percent); + } + + last_processed_counter_ = counter; + return true; +} + +} // namespace b_parasite +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h new file mode 100644 index 0000000000..04f648ab63 --- /dev/null +++ b/esphome/components/b_parasite/b_parasite.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace b_parasite { + +class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_soil_moisture(sensor::Sensor *soil_moisture) { soil_moisture_ = soil_moisture; } + + protected: + // The received advertisement packet contains an unsigned 4 bits wrap-around counter + // for deduplicating messages. + int8_t last_processed_counter_ = -1; + uint64_t address_; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *soil_moisture_{nullptr}; +}; + +} // namespace b_parasite +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py new file mode 100644 index 0000000000..d90ea84cd3 --- /dev/null +++ b/esphome/components/b_parasite/sensor.py @@ -0,0 +1,68 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_VOLTAGE, + CONF_HUMIDITY, + CONF_ID, + CONF_MOISTURE, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@rbaron"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +b_parasite_ns = cg.esphome_ns.namespace("b_parasite") +BParasite = b_parasite_ns.class_( + "BParasite", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BParasite), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE + ), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + for (config_key, setter) in [ + (CONF_TEMPERATURE, var.set_temperature), + (CONF_HUMIDITY, var.set_humidity), + (CONF_BATTERY_VOLTAGE, var.set_battery_voltage), + (CONF_MOISTURE, var.set_soil_moisture), + ]: + if config_key in config: + sens = yield sensor.new_sensor(config[config_key]) + cg.add(setter(sens)) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 2ec297a6f4..799440fdfc 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -17,8 +17,8 @@ void BH1750Sensor::setup() { return; } - uint8_t mtreg_hi = (this->measurement_time_ >> 5) & 0b111; - uint8_t mtreg_lo = (this->measurement_time_ >> 0) & 0b11111; + uint8_t mtreg_hi = (this->measurement_duration_ >> 5) & 0b111; + uint8_t mtreg_lo = (this->measurement_duration_ >> 0) & 0b11111; this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); } @@ -77,7 +77,7 @@ void BH1750Sensor::read_data_() { } float lx = float(raw_value) / 1.2f; - lx *= 69.0f / this->measurement_time_; + lx *= 69.0f / this->measurement_duration_; ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); this->publish_state(lx); this->status_clear_warning(); diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 00abd53e92..c88fa10832 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -28,7 +28,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: * @param resolution The new resolution of the sensor. */ void set_resolution(BH1750Resolution resolution); - void set_measurement_time(uint8_t measurement_time) { measurement_time_ = measurement_time; } + void set_measurement_duration(uint8_t measurement_duration) { measurement_duration_ = measurement_duration; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -41,7 +41,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void read_data_(); BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; - uint8_t measurement_time_; + uint8_t measurement_duration_; }; } // namespace bh1750 diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index 9ad1b7eadb..0e37b2b865 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( DEVICE_CLASS_ILLUMINANCE, ICON_EMPTY, UNIT_LUX, + CONF_MEASUREMENT_DURATION, ) DEPENDENCIES = ["i2c"] @@ -32,9 +33,12 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum( BH1750_RESOLUTIONS, float=True ), - cv.Optional(CONF_MEASUREMENT_TIME, default=69): cv.int_range( + cv.Optional(CONF_MEASUREMENT_DURATION, default=69): cv.int_range( min=31, max=254 ), + cv.Optional(CONF_MEASUREMENT_TIME): cv.invalid( + "The 'measurement_time' option has been replaced with 'measurement_duration' in 1.18.0" + ), } ) .extend(cv.polling_component_schema("60s")) @@ -49,4 +53,4 @@ def to_code(config): yield i2c.register_i2c_device(var, config) cg.add(var.set_resolution(config[CONF_RESOLUTION])) - cg.add(var.set_measurement_time(config[CONF_MEASUREMENT_TIME])) + cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION])) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py new file mode 100644 index 0000000000..d3b287574b --- /dev/null +++ b/esphome/components/ble_client/__init__.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import ( + CONF_ID, + CONF_MAC_ADDRESS, + CONF_NAME, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_TRIGGER_ID, +) +from esphome.core import coroutine +from esphome import automation + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["esp32_ble_tracker"] + +ble_client_ns = cg.esphome_ns.namespace("ble_client") +BLEClient = ble_client_ns.class_( + "BLEClient", cg.Component, esp32_ble_tracker.ESPBTClient +) +BLEClientNode = ble_client_ns.class_("BLEClientNode") +BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const") +# Triggers +BLEClientConnectTrigger = ble_client_ns.class_( + "BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) +BLEClientDisconnectTrigger = ble_client_ns.class_( + "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) + +# Espressif platformio framework is built with MAX_BLE_CONN to 3, so +# enforce this in yaml checks. +MULTI_CONF = 3 + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEClient), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientConnectTrigger + ), + } + ), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientDisconnectTrigger + ), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) +) + +CONF_BLE_CLIENT_ID = "ble_client_id" + +BLE_CLIENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient), + } +) + + +@coroutine +def register_ble_node(var, config): + parent = yield cg.get_variable(config[CONF_BLE_CLIENT_ID]) + cg.add(parent.register_ble_node(var)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_client(var, config) + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + for conf in config.get(CONF_ON_CONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DISCONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h new file mode 100644 index 0000000000..2db609de55 --- /dev/null +++ b/esphome/components/ble_client/automation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/ble_client.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { +class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_OPEN_EVT && param->open.status == ESP_GATT_OK) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::Established; + } +}; + +class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_DISCONNECT_EVT && memcmp(param->disconnect.remote_bda, this->parent_->remote_bda, 6) == 0) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::Established; + } +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp new file mode 100644 index 0000000000..3819a6e560 --- /dev/null +++ b/esphome/components/ble_client/ble_client.cpp @@ -0,0 +1,392 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "ble_client.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_client"; + +void BLEClient::setup() { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_states(espbt::ClientState::Idle); + this->enabled = true; +} + +void BLEClient::loop() { + if (this->state() == espbt::ClientState::Discovered) { + this->connect(); + } + for (auto *node : this->nodes_) + node->loop(); +} + +void BLEClient::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Client:"); + ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); +} + +bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { + if (!this->enabled) + return false; + if (device.address_uint64() != this->address) + return false; + if (this->state() != espbt::ClientState::Idle) + return false; + + ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str()); + this->set_states(espbt::ClientState::Discovered); + + auto addr = device.address_uint64(); + this->remote_bda[0] = (addr >> 40) & 0xFF; + this->remote_bda[1] = (addr >> 32) & 0xFF; + this->remote_bda[2] = (addr >> 24) & 0xFF; + this->remote_bda[3] = (addr >> 16) & 0xFF; + this->remote_bda[4] = (addr >> 8) & 0xFF; + this->remote_bda[5] = (addr >> 0) & 0xFF; + return true; +} + +std::string BLEClient::address_str() const { + char buf[20]; + sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", (uint8_t)(this->address >> 40) & 0xff, + (uint8_t)(this->address >> 32) & 0xff, (uint8_t)(this->address >> 24) & 0xff, + (uint8_t)(this->address >> 16) & 0xff, (uint8_t)(this->address >> 8) & 0xff, + (uint8_t)(this->address >> 0) & 0xff); + std::string ret; + ret = buf; + return ret; +} + +void BLEClient::set_enabled(bool enabled) { + if (enabled == this->enabled) + return; + if (!enabled && this->state() != espbt::ClientState::Idle) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + auto ret = esp_ble_gattc_close(this->gattc_if, this->conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); + } + } + this->enabled = enabled; +} + +void BLEClient::connect() { + ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); + auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, BLE_ADDR_TYPE_PUBLIC, true); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); + this->set_states(espbt::ClientState::Idle); + } else { + this->set_states(espbt::ClientState::Connecting); + } +} + +void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return; + if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && gattc_if != this->gattc_if) + return; + + bool all_established = this->all_nodes_established(); + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "gattc registered app id %d", this->app_id); + this->gattc_if = esp_gattc_if; + } else { + ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); + if (param->open.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); + this->set_states(espbt::ClientState::Idle); + break; + } + this->conn_id = param->open.conn_id; + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret); + } + break; + } + case ESP_GATTC_CFG_MTU_EVT: { + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status); + this->set_states(espbt::ClientState::Idle); + break; + } + ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu); + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, NULL); + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { + return; + } + ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) + delete svc; + this->services_.clear(); + this->set_states(espbt::ClientState::Idle); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + BLEService *ble_service = new BLEService(); + ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); + ble_service->start_handle = param->search_res.start_handle; + ble_service->end_handle = param->search_res.end_handle; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) { + ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str()); + ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle); + svc->parse_characteristics(); + } + this->set_states(espbt::ClientState::Connected); + this->set_state(espbt::ClientState::Established); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + auto descr = this->get_config_descriptor(param->reg_for_notify.handle); + if (descr == nullptr) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle); + break; + } + if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle, + descr->uuid.to_string().c_str()); + break; + } + uint8_t notify_en = 1; + auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), + ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + } + break; + } + + default: + break; + } + for (auto *node : this->nodes_) + node->gattc_event_handler(event, esp_gattc_if, param); + + // Delete characteristics after clients have used them to save RAM. + if (!all_established && this->all_nodes_established()) { + for (auto &svc : this->services_) + delete svc; + this->services_.clear(); + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + if (length > 2) { + return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); + } + case 0x7: // uint24. + if (length > 3) { + return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); + } + case 0x8: // uint32. + if (length > 4) { + return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + + (uint32_t)(value[4])); + } + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + if (length > 2) { + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + } + case 0xF: // int24. + if (length > 3) { + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + } + case 0x10: // int32. + if (length > 4) { + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } + } + ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length); + return NAN; +} + +BLEService *BLEClient::get_service(espbt::ESPBTUUID uuid) { + for (auto svc : this->services_) + if (svc->uuid == uuid) + return svc; + return nullptr; +} + +BLEService *BLEClient::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClient::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLEDescriptor *BLEClient::get_config_descriptor(uint16_t handle) { + for (auto &svc : this->services_) + for (auto &chr : svc->characteristics) + if (chr->handle == handle) + for (auto &desc : chr->descriptors) + if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902)) + return desc; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + for (auto &chr : this->characteristics) + if (chr->uuid == uuid) + return chr; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEDescriptor *BLEClient::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; +} + +void BLEService::parse_characteristics() { + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_char( + this->client->gattc_if, this->client->conn_id, this->start_handle, this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLECharacteristic *characteristic = new BLECharacteristic(); + characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + characteristic->properties = result.properties; + characteristic->handle = result.char_handle; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(), + characteristic->handle, characteristic->properties); + characteristic->parse_descriptors(); + offset++; + } +} + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; +} + +void BLECharacteristic::parse_descriptors() { + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_descr( + this->service->client->gattc_if, this->service->client->conn_id, this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + for (auto &desc : this->descriptors) + if (desc->uuid == uuid) + return desc; + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h new file mode 100644 index 0000000000..203acc181f --- /dev/null +++ b/esphome/components/ble_client/ble_client.h @@ -0,0 +1,140 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClient; +class BLEService; +class BLECharacteristic; + +class BLEClientNode { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void loop() = 0; + void set_address(uint64_t address) { address_ = address; } + espbt::ESPBTClient *client; + // This should be transitioned to Established once the node no longer needs + // the services/descriptors/characteristics of the parent client. This will + // allow some memory to be freed. + espbt::ClientState node_state; + + BLEClient *parent() { return this->parent_; } + void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } + + protected: + BLEClient *parent_; + uint64_t address_; +}; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + + BLEService *service; +}; + +class BLEService { + public: + ~BLEService(); + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClient *client; + void parse_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +class BLEClient : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void dump_config() override; + void loop() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + void connect(); + + void set_address(uint64_t address) { this->address = address; } + + void set_enabled(bool enabled); + + void register_ble_node(BLEClientNode *node) { + node->client = this; + node->set_ble_client_parent(this); + this->nodes_.push_back(node); + } + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int gattc_if; + esp_bd_addr_t remote_bda; + uint16_t conn_id; + uint64_t address; + bool enabled; + std::string address_str() const; + + protected: + void set_states(espbt::ClientState st) { + this->set_state(st); + for (auto &node : nodes_) + node->node_state = st; + } + bool all_nodes_established() { + if (this->state() != espbt::ClientState::Established) + return false; + for (auto &node : nodes_) + if (node->node_state != espbt::ClientState::Established) + return false; + return true; + } + + std::vector nodes_; + std::vector services_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py new file mode 100644 index 0000000000..27eca87a37 --- /dev/null +++ b/esphome/components/ble_client/sensor/__init__.py @@ -0,0 +1,115 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client, esp32_ble_tracker +from esphome.const import ( + DEVICE_CLASS_EMPTY, + CONF_ID, + UNIT_EMPTY, + ICON_EMPTY, + CONF_TRIGGER_ID, + CONF_SERVICE_UUID, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" + +BLESensor = ble_client_ns.class_( + "BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode +) +BLESensorNotifyTrigger = ble_client_ns.class_( + "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(BLESensor), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLESensorNotifyTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) + cg.add(var.set_descr_uuid128(uuid128)) + + yield cg.register_component(var, config) + yield ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + yield sensor.register_sensor(var, config) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield ble_client.register_ble_node(trigger, config) + yield automation.build_automation(trigger, [(float, "x")], conf) diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h new file mode 100644 index 0000000000..a528493947 --- /dev/null +++ b/esphome/components/ble_client/sensor/automation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/sensor/ble_sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +class BLESensorNotifyTrigger : public Trigger, public BLESensor { + public: + explicit BLESensorNotifyTrigger(BLESensor *sensor) { sensor_ = sensor; } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::Established; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLESensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp new file mode 100644 index 0000000000..ef1d6c120f --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -0,0 +1,129 @@ +#include "ble_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_sensor"; + +uint32_t BLESensor::hash_base() { return 343459825UL; } + +void BLESensor::loop() {} + +void BLESensor::dump_config() { + LOG_SENSOR("", "BLE Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = + esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::Established; + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + this->status_clear_warning(); + this->publish_state((float) param->read.value[0]); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + break; + ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + this->publish_state((float) param->notify.value[0]); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::Established; + break; + } + default: + break; + } +} + +void BLESensor::update() { + if (this->node_state != espbt::ClientState::Established) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h new file mode 100644 index 0000000000..e3a8a8ea25 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_enable_notify(bool notify) { this->notify_ = notify; } + uint16_t handle; + + protected: + uint32_t hash_base() override; + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py new file mode 100644 index 0000000000..acc8683407 --- /dev/null +++ b/esphome/components/ble_client/switch/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch, ble_client +from esphome.const import CONF_ICON, CONF_ID, CONF_INVERTED, ICON_BLUETOOTH +from .. import ble_client_ns + +BLEClientSwitch = ble_client_ns.class_( + "BLEClientSwitch", switch.Switch, cg.Component, ble_client.BLEClientNode +) + +CONFIG_SCHEMA = ( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLEClientSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "BLE client switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_BLUETOOTH): switch.icon, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield switch.register_switch(var, config) + yield ble_client.register_ble_node(var, config) diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp new file mode 100644 index 0000000000..ef7b64be66 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -0,0 +1,39 @@ +#include "ble_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_switch"; + +void BLEClientSwitch::write_state(bool state) { + this->parent_->set_enabled(state); + this->publish_state(state); +} + +void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + this->publish_state(this->parent_->enabled); + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::Established; + break; + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::Idle; + this->publish_state(this->parent_->enabled); + break; + default: + break; + } +} + +void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h new file mode 100644 index 0000000000..f91af533f1 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/switch/switch.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientSwitch : public switch_::Switch, public Component, public BLEClientNode { + public: + void dump_config() override; + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py new file mode 100644 index 0000000000..8286029c3b --- /dev/null +++ b/esphome/components/bme680_bsec/__init__.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@trvrnrth"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_BME680_BSEC_ID = "bme680_bsec_id" +CONF_TEMPERATURE_OFFSET = "temperature_offset" +CONF_IAQ_MODE = "iaq_mode" +CONF_SAMPLE_RATE = "sample_rate" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" + +bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec") + +IAQMode = bme680_bsec_ns.enum("IAQMode") +IAQ_MODE_OPTIONS = { + "STATIC": IAQMode.IAQ_MODE_STATIC, + "MOBILE": IAQMode.IAQ_MODE_MOBILE, +} + +SampleRate = bme680_bsec_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +BME680BSECComponent = bme680_bsec_ns.class_( + "BME680BSECComponent", cg.Component, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( + IAQ_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + } +).extend(i2c.i2c_device_schema(0x76)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + cg.add_define("USING_BSEC") + cg.add_library("BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp new file mode 100644 index 0000000000..a463ff78c4 --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -0,0 +1,396 @@ + + +#include "bme680_bsec.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome { +namespace bme680_bsec { +#ifdef USING_BSEC +static const char *TAG = "bme680_bsec.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +BME680BSECComponent *BME680BSECComponent::instance; + +void BME680BSECComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); + BME680BSECComponent::instance = this; + + this->bsec_status_ = bsec_init(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->bme680_.dev_id = this->address_; + this->bme680_.intf = BME680_I2C_INTF; + this->bme680_.read = BME680BSECComponent::read_bytes_wrapper; + this->bme680_.write = BME680BSECComponent::write_bytes_wrapper; + this->bme680_.delay_ms = BME680BSECComponent::delay_ms; + this->bme680_.amb_temp = 25; + this->bme680_.power_mode = BME680_FORCED_MODE; + + this->bme680_status_ = bme680_init(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + this->mark_failed(); + return; + } + + if (this->sample_rate_ == SAMPLE_RATE_ULP) { + const uint8_t bsec_config[] = { +#include "config/generic_33v_300s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + this->update_subscription_(BSEC_SAMPLE_RATE_ULP); + } else { + const uint8_t bsec_config[] = { +#include "config/generic_33v_3s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + this->update_subscription_(BSEC_SAMPLE_RATE_LP); + } + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->load_state_(); +} + +void BME680BSECComponent::set_config_(const uint8_t *config) { + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer)); +} + +void BME680BSECComponent::update_subscription_(float sample_rate) { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + int num_virtual_sensors = 0; + + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = + this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = + bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); +} + +void BME680BSECComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BME680 via BSEC:"); + + bsec_version_t version; + bsec_get_version(&version); + ESP_LOGCONFIG(TAG, " BSEC Version: %d.%d.%d.%d", version.major, version.minor, version.major_bugfix, + version.minor_bugfix); + + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC Status: %d, BME680 Status: %d)", this->bsec_status_, + this->bme680_status_); + } + + ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); + ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", this->sample_rate_ == SAMPLE_RATE_ULP ? "ULP" : "LP"); + ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_); + LOG_TEXT_SENSOR(" ", "IAQ Accuracy", this->iaq_accuracy_text_sensor_); + LOG_SENSOR(" ", "CO2 Equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); +} + +float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } + +void BME680BSECComponent::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme680_status_ < BME680_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme680_status_ > BME680_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +void BME680BSECComponent::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + + ESP_LOGV(TAG, "Performing sensor run"); + + bsec_bme_settings_t bme680_settings; + this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = bme680_settings.next_call; + + this->bme680_.gas_sett.run_gas = bme680_settings.run_gas; + this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling; + this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling; + this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling; + this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature; + this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration; + uint16_t desired_settings = + BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_FILTER_SEL | BME680_GAS_SENSOR_SEL; + this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_); + return; + } + + this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; + } + + uint16_t meas_dur = 0; + bme680_get_profile_dur(&meas_dur, &this->bme680_); + ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); + this->set_timeout("read", meas_dur, [this, bme680_settings]() { this->read_(bme680_settings); }); +} + +void BME680BSECComponent::read_(bsec_bme_settings_t bme680_settings) { + ESP_LOGV(TAG, "Reading data"); + struct bme680_field_data data; + this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_); + + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME680 Error Code %d)", this->bme680_status_); + return; + } + if (!(data.status & BME680_NEW_DATA_MSK)) { + ESP_LOGD(TAG, "BME680 did not report new data"); + return; + } + + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + int64_t curr_time_ns = this->get_time_ns_(); + + if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data.temperature / 100.0f; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + + // Temperature offset from the real temperature due to external heat sources + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data.humidity / 1000.0f; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data.pressure; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_GAS) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data.gas_resistance; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC"); + return; + } + + this->publish_(outputs, num_outputs); +} + +void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + for (uint8_t i = 0; i < num_outputs; i++) { + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + case BSEC_OUTPUT_STATIC_IAQ: + uint8_t accuracy; + accuracy = outputs[i].accuracy; + this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); + this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); + this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); + + // Queue up an opportunity to save state + this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: + this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: + this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_RAW_PRESSURE: + this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); + break; + case BSEC_OUTPUT_RAW_GAS: + this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: + this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: + this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); + break; + } + } +} + +int64_t BME680BSECComponent::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME680BSECComponent::delay_ms(uint32_t period) { + ESP_LOGV(TAG, "Delaying for %ums", period); + delay(period); +} + +void BME680BSECComponent::load_state_() { + uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_)); + this->bsec_state_ = global_preferences.make_preference(hash, true); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME680BSECComponent::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = + bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} +#endif +} // namespace bme680_bsec +} // namespace esphome diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h new file mode 100644 index 0000000000..4a71e1d23b --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -0,0 +1,108 @@ + + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/preferences.h" +#include + +#ifdef USING_BSEC +#include +#endif + +namespace esphome { +namespace bme680_bsec { +#ifdef USING_BSEC + +enum IAQMode { + IAQ_MODE_STATIC = 0, + IAQ_MODE_MOBILE = 1, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, +}; + +class BME680BSECComponent : public Component, public i2c::I2CDevice { + public: + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_gas_resistance_sensor(sensor::Sensor *gas_resistance_sensor) { + gas_resistance_sensor_ = gas_resistance_sensor; + } + void set_iaq_sensor(sensor::Sensor *iaq_sensor) { iaq_sensor_ = iaq_sensor; } + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *iaq_accuracy_text_sensor) { + iaq_accuracy_text_sensor_ = iaq_accuracy_text_sensor; + } + void set_iaq_accuracy_sensor(sensor::Sensor *iaq_accuracy_sensor) { iaq_accuracy_sensor_ = iaq_accuracy_sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *co2_equivalent_sensor) { + co2_equivalent_sensor_ = co2_equivalent_sensor; + } + void set_breath_voc_equivalent_sensor(sensor::Sensor *breath_voc_equivalent_sensor) { + breath_voc_equivalent_sensor_ = breath_voc_equivalent_sensor; + } + + static BME680BSECComponent *instance; + static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static void delay_ms(uint32_t period); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void set_config_(const uint8_t *config); + void update_subscription_(float sample_rate); + + void run_(); + void read_(bsec_bme_settings_t bme680_settings); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + int64_t get_time_ns_(); + + void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); + void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); + + void load_state_(); + void save_state_(uint8_t accuracy); + + struct bme680_dev bme680_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme680_status_{BME680_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + IAQMode iaq_mode_{IAQ_MODE_STATIC}; + SampleRate sample_rate_{SAMPLE_RATE_LP}; + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + sensor::Sensor *humidity_sensor_; + sensor::Sensor *gas_resistance_sensor_; + sensor::Sensor *iaq_sensor_; + text_sensor::TextSensor *iaq_accuracy_text_sensor_; + sensor::Sensor *iaq_accuracy_sensor_; + sensor::Sensor *co2_equivalent_sensor_; + sensor::Sensor *breath_voc_equivalent_sensor_; +}; +#endif +} // namespace bme680_bsec +} // namespace esphome diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py new file mode 100644 index 0000000000..b27b267c11 --- /dev/null +++ b/esphome/components/bme680_bsec/sensor.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, +) +from esphome.core import coroutine +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ = "iaq" +CONF_IAQ_ACCURACY = "iaq_accuracy" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +UNIT_IAQ = "IAQ" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" + +TYPES = { + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_PRESSURE: "set_pressure_sensor", + CONF_HUMIDITY: "set_humidity_sensor", + CONF_GAS_RESISTANCE: "set_gas_resistance_sensor", + CONF_IAQ: "set_iaq_sensor", + CONF_IAQ_ACCURACY: "set_iaq_accuracy_sensor", + CONF_CO2_EQUIVALENT: "set_co2_equivalent_sensor", + CONF_BREATH_VOC_EQUIVALENT: "set_breath_voc_equivalent_sensor", +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_WATER_PERCENT, 1, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY + ), + } +) + + +@coroutine +def setup_conf(config, key, hub, funcName): + if key in config: + conf = config[key] + var = yield sensor.new_sensor(conf) + func = getattr(hub, funcName) + cg.add(func(var)) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key, funcName in TYPES.items(): + yield setup_conf(config, key, hub, funcName) diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py new file mode 100644 index 0000000000..992e2989c9 --- /dev/null +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID, CONF_ICON +from esphome.core import coroutine +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ_ACCURACY = "iaq_accuracy" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = {CONF_IAQ_ACCURACY: "set_iaq_accuracy_text_sensor"} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon, + } + ), + } +) + + +@coroutine +def setup_conf(config, key, hub, funcName): + if key in config: + conf = config[key] + var = cg.new_Pvariable(conf[CONF_ID]) + yield text_sensor.register_text_sensor(var, conf) + func = getattr(hub, funcName) + cg.add(func(var)) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key, funcName in TYPES.items(): + yield setup_conf(config, key, hub, funcName) diff --git a/esphome/components/custom_component/custom_component.h b/esphome/components/custom_component/custom_component.h index 6b009ba549..3f5760e4cf 100644 --- a/esphome/components/custom_component/custom_component.h +++ b/esphome/components/custom_component/custom_component.h @@ -16,7 +16,7 @@ class CustomComponentConstructor { } } - Component *get_component(int i) { return this->components_[i]; } + Component *get_component(int i) const { return this->components_[i]; } protected: std::vector components_; diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index f72ec88fae..fe8bc9bd7c 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_PAGE_ID, CONF_ROTATION from esphome.core import coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -19,6 +19,9 @@ DisplayPageShowNextAction = display_ns.class_( DisplayPageShowPrevAction = display_ns.class_( "DisplayPageShowPrevAction", automation.Action ) +DisplayIsDisplayingPageCondition = display_ns.class_( + "DisplayIsDisplayingPageCondition", automation.Condition +) DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -125,6 +128,26 @@ def display_page_show_previous_to_code(config, action_id, template_arg, args): yield cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_condition( + "display.is_displaying_page", + DisplayIsDisplayingPageCondition, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer), + cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage), + }, + key=CONF_PAGE_ID, + ), +) +def display_is_displaying_page_to_code(config, condition_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + page = yield cg.get_variable(config[CONF_PAGE_ID]) + var = cg.new_Pvariable(condition_id, template_arg, paren) + cg.add(var.set_page(page)) + + yield var + + @coroutine_with_priority(100.0) def to_code(config): cg.add_global(display_ns.using) diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 5a63441e2d..71a6189990 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -296,6 +296,8 @@ class DisplayBuffer { void set_pages(std::vector pages); + const DisplayPage *get_active_page() const { return this->page_; } + /// Internal method to set the display rotation with. void set_rotation(DisplayRotation rotation); @@ -448,5 +450,17 @@ template class DisplayPageShowPrevAction : public Action DisplayBuffer *buffer_; }; +template class DisplayIsDisplayingPageCondition : public Condition { + public: + DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {} + + void set_page(DisplayPage *page) { this->page_ = page; } + bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + + protected: + DisplayBuffer *parent_; + DisplayPage *page_; +}; + } // namespace display } // namespace esphome diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2e567b49bc..8726ab3e8d 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -26,6 +26,7 @@ CONF_WINDOW = "window" CONF_ACTIVE = "active" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const") @@ -220,3 +221,10 @@ def register_ble_device(var, config): paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) yield var + + +@coroutine +def register_client(var, config): + paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + yield var diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b2d303da15..7a5b023387 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -48,7 +48,23 @@ void ESP32BLETracker::setup() { } void ESP32BLETracker::loop() { - if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + BLEEvent *ble_event = this->ble_events_.pop(); + while (ble_event != nullptr) { + if (ble_event->type_) + this->real_gattc_event_handler(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + else + this->real_gap_event_handler(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + delete ble_event; + ble_event = this->ble_events_.pop(); + } + + bool connecting = false; + for (auto *client : this->clients_) { + if (client->state() == ClientState::Connecting || client->state() == ClientState::Discovered) + connecting = true; + } + if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { xSemaphoreGive(this->scan_end_lock_); global_esp32_ble_tracker->start_scan(false); } @@ -69,6 +85,17 @@ void ESP32BLETracker::loop() { if (listener->parse_device(device)) found = true; + for (auto *client : this->clients_) + if (client->parse_device(device)) { + found = true; + if (client->state() == ClientState::Discovered) { + esp_ble_gap_stop_scanning(); + if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { + xSemaphoreGive(this->scan_end_lock_); + } + } + } + if (!found) { this->print_bt_device_info(device); } @@ -122,6 +149,11 @@ bool ESP32BLETracker::ble_setup() { ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); return false; } + err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } // Empty name esp_ble_gap_set_device_name(""); @@ -166,7 +198,17 @@ void ESP32BLETracker::start_scan(bool first) { }); } +void ESP32BLETracker::register_client(ESPBTClient *client) { + client->app_id = ++this->app_id_; + this->clients_.push_back(client); +} + void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEEvent *gap_event = new BLEEvent(event, param); + global_esp32_ble_tracker->ble_events_.push(gap_event); +} + +void ESP32BLETracker::real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: global_esp32_ble_tracker->gap_scan_result(param->scan_rst); @@ -177,6 +219,9 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: global_esp32_ble_tracker->gap_scan_start_complete(param->scan_start_cmpl); break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + global_esp32_ble_tracker->gap_scan_stop_complete(param->scan_stop_cmpl); + break; default: break; } @@ -190,6 +235,10 @@ void ESP32BLETracker::gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_ this->scan_start_failed_ = param.status; } +void ESP32BLETracker::gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + xSemaphoreGive(this->scan_end_lock_); +} + void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { @@ -203,6 +252,19 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } +void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); + global_esp32_ble_tracker->ble_events_.push(gattc_event); +} + +void ESP32BLETracker::real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + for (auto *client : global_esp32_ble_tracker->clients_) { + client->gattc_event_handler(event, gattc_if, param); + } +} + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -223,6 +285,15 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ret.uuid_.uuid.uuid128[i] = data[i]; return ret; } +ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = uuid.len; + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + return ret; +} ESPBTUUID ESPBTUUID::as_128bit() const { if (this->uuid_.len == ESP_UUID_LEN_128) { return *this; @@ -289,16 +360,21 @@ std::string ESPBTUUID::to_string() { char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); break; case ESP_UUID_LEN_32: - sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); break; default: case ESP_UUID_LEN_128: - for (uint8_t i = 0; i < 16; i++) - sprintf(sbuf + i * 3, "%02X:", this->uuid_.uuid.uuid128[i]); + char *bpos = sbuf; + for (int8_t i = 15; i >= 0; i--) { + sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); + bpos += 2; + if (i == 3 || i == 5 || i == 7 || i == 9) + sprintf(bpos++, "-"); + } sbuf[47] = '\0'; break; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index eef7930b78..6f0c28a73c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -2,12 +2,14 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "queue.h" #ifdef ARDUINO_ARCH_ESP32 #include #include #include +#include #include namespace esphome { @@ -23,6 +25,8 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); + ESPBTUUID as_128bit() const; bool contains(uint8_t data1, uint8_t data2) const; @@ -135,6 +139,32 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; +enum class ClientState { + // Connection is idle, no device detected. + Idle, + // Device advertisement found. + Discovered, + // Connection in progress. + Connecting, + // Initial connection established. + Connected, + // The client and sub-clients have completed setup. + Established, +}; + +class ESPBTClient : public ESPBTDeviceListener { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void connect() = 0; + void set_state(ClientState st) { this->state_ = st; } + ClientState state() const { return state_; } + int app_id; + + protected: + ClientState state_; +}; + class ESP32BLETracker : public Component { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -153,6 +183,8 @@ class ESP32BLETracker : public Component { this->listeners_.push_back(listener); } + void register_client(ESPBTClient *client); + void print_bt_device_info(const ESPBTDevice &device); protected: @@ -162,16 +194,26 @@ class ESP32BLETracker : public Component { void start_scan(bool first); /// Callback that will handle all GAP events and redistribute them to other callbacks. static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. void gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received. void gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); + /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. + void gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); + + int app_id_; + /// Callback that will handle all GATTC events and redistribute them to other callbacks. + static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; std::vector listeners_; + /// Client parameters. + std::vector clients_; /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; /// The interval in seconds to perform scans. @@ -185,6 +227,8 @@ class ESP32BLETracker : public Component { esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + Queue ble_events_; }; extern ESP32BLETracker *global_esp32_ble_tracker; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h new file mode 100644 index 0000000000..6f36cf874d --- /dev/null +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -0,0 +1,96 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +/* + * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather + * than trying to deal wth various locking strategies, all incoming GAP and GATT + * events will simply be placed on a semaphore guarded queue. The next time the + * component runs loop(), these events are popped off the queue and handed at + * this safer time. + */ + +namespace esphome { +namespace esp32_ble_tracker { + +template class Queue { + public: + Queue() { m = xSemaphoreCreateMutex(); } + + void push(T *element) { + if (element == nullptr) + return; + if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { + q.push(element); + xSemaphoreGive(m); + } + } + + T *pop() { + T *element = nullptr; + + if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { + if (!q.empty()) { + element = q.front(); + q.pop(); + } + xSemaphoreGive(m); + } + return element; + } + + protected: + std::queue q; + SemaphoreHandle_t m; +}; + +// Received GAP and GATTC events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = 0; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of notify event data. + if (e == ESP_GATTC_NOTIFY_EVT) { + memcpy(this->event_.gattc.notify_data, p->notify.value, p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->event_.gattc.notify_data; + } + this->type_ = 1; + }; + + union { + struct gap_event { + esp_gap_ble_cb_event_t gap_event; + esp_ble_gap_cb_param_t gap_param; + } gap; + + struct gattc_event { + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t gattc_param; + uint8_t notify_data[64]; + } gattc; + } event_; + uint8_t type_; // 0=gap 1=gattc +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ca00f33359..82366eeac2 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.config_validation as cv import esphome.codegen as cg +from esphome.components.network import add_mdns_library from esphome.const import ( CONF_DOMAIN, CONF_ID, @@ -9,6 +10,7 @@ from esphome.const import ( CONF_TYPE, CONF_USE_ADDRESS, ESP_PLATFORM_ESP32, + CONF_ENABLE_MDNS, CONF_GATEWAY, CONF_SUBNET, CONF_DNS1, @@ -80,6 +82,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, + cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional("hostname"): cv.invalid( @@ -122,3 +125,6 @@ def to_code(config): cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) cg.add_define("USE_ETHERNET") + + if config[CONF_ENABLE_MDNS]: + add_mdns_library() diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0553d66273..005712420f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -33,7 +33,9 @@ void EthernetComponent::setup() { this->start_connect_(); +#ifdef USE_MDNS network_setup_mdns(); +#endif } void EthernetComponent::loop() { const uint32_t now = millis(); diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py new file mode 100644 index 0000000000..272812adcf --- /dev/null +++ b/esphome/components/external_components/__init__.py @@ -0,0 +1,197 @@ +import re +import logging +from pathlib import Path +import subprocess +import hashlib +import datetime + +import esphome.config_validation as cv +from esphome.const import ( + CONF_COMPONENTS, + CONF_SOURCE, + CONF_URL, + CONF_TYPE, + CONF_EXTERNAL_COMPONENTS, + CONF_PATH, +) +from esphome.core import CORE +from esphome import loader + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = CONF_EXTERNAL_COMPONENTS + +TYPE_GIT = "git" +TYPE_LOCAL = "local" +CONF_REFRESH = "refresh" +CONF_REF = "ref" + + +def validate_git_ref(value): + if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + raise cv.Invalid("Not a valid git ref") + return value + + +GIT_SCHEMA = { + cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_REF): validate_git_ref, +} +LOCAL_SCHEMA = { + cv.Required(CONF_PATH): cv.directory, +} + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + try: + return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except cv.Invalid: + pass + # Regex for GitHub repo name with optional branch/tag + # Note: git allows other branch/tag names as well, but never seen them used before + m = re.match( + r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?", + value, + ) + if m is None: + raise cv.Invalid( + "Source is not a file system path or in expected github://username/name[@branch-or-tag] format!" + ) + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + } + if m.group(3): + conf[CONF_REF] = m.group(3) + return SOURCE_SCHEMA(conf) + + +def validate_refresh(value: str): + if value.lower() == "always": + return validate_refresh("0s") + if value.lower() == "never": + return validate_refresh("1000y") + return cv.positive_time_period_seconds(value) + + +SOURCE_SCHEMA = cv.Any( + validate_source_shorthand, + cv.typed_schema( + { + TYPE_GIT: cv.Schema(GIT_SCHEMA), + TYPE_LOCAL: cv.Schema(LOCAL_SCHEMA), + } + ), +) + + +CONFIG_SCHEMA = cv.ensure_list( + { + cv.Required(CONF_SOURCE): SOURCE_SCHEMA, + cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh), + cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( + "all", cv.ensure_list(cv.string) + ), + } +) + + +def to_code(config): + pass + + +def _compute_destination_path(key: str) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN + h = hashlib.new("sha256") + h.update(key.encode()) + return base_dir / h.hexdigest()[:8] + + +def _handle_git_response(ret): + if ret.returncode != 0 and ret.stderr: + err_str = ret.stderr.decode("utf-8") + lines = [x.strip() for x in err_str.splitlines()] + if lines[-1].startswith("fatal:"): + raise cv.Invalid(lines[-1][len("fatal: ") :]) + raise cv.Invalid(err_str) + + +def _process_single_config(config: dict): + conf = config[CONF_SOURCE] + if conf[CONF_TYPE] == TYPE_GIT: + key = f"{conf[CONF_URL]}@{conf.get(CONF_REF)}" + repo_dir = _compute_destination_path(key) + if not repo_dir.is_dir(): + cmd = ["git", "clone", "--depth=1"] + if CONF_REF in conf: + cmd += ["--branch", conf[CONF_REF]] + cmd += [conf[CONF_URL], str(repo_dir)] + ret = subprocess.run(cmd, capture_output=True, check=False) + _handle_git_response(ret) + + else: + # Check refresh needed + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") + # On first clone, FETCH_HEAD does not exists + if not file_timestamp.exists(): + file_timestamp = Path(repo_dir / ".git" / "HEAD") + age = datetime.datetime.now() - datetime.datetime.fromtimestamp( + file_timestamp.stat().st_mtime + ) + if age.seconds > config[CONF_REFRESH].total_seconds: + _LOGGER.info("Executing git pull %s", key) + cmd = ["git", "pull"] + ret = subprocess.run( + cmd, cwd=repo_dir, capture_output=True, check=False + ) + _handle_git_response(ret) + + if (repo_dir / "esphome" / "components").is_dir(): + components_dir = repo_dir / "esphome" / "components" + elif (repo_dir / "components").is_dir(): + components_dir = repo_dir / "components" + else: + raise cv.Invalid( + "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder", + [CONF_SOURCE], + ) + + elif conf[CONF_TYPE] == TYPE_LOCAL: + components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) + else: + raise NotImplementedError() + + if config[CONF_COMPONENTS] == "all": + num_components = len(list(components_dir.glob("*/__init__.py"))) + if num_components > 100: + # Prevent accidentally including all components from an esphome fork/branch + # In this case force the user to manually specify which components they want to include + raise cv.Invalid( + "This source is an ESPHome fork or branch. Please manually specify the components you want to import using the 'components' key", + [CONF_COMPONENTS], + ) + allowed_components = None + else: + for i, name in enumerate(config[CONF_COMPONENTS]): + expected = components_dir / name / "__init__.py" + if not expected.is_file(): + raise cv.Invalid( + f"Could not find __init__.py file for component {name}. Please check the component is defined by this source (search path: {expected})", + [CONF_COMPONENTS, i], + ) + allowed_components = config[CONF_COMPONENTS] + + loader.install_meta_finder(components_dir, allowed_components=allowed_components) + + +def do_external_components_pass(config: dict) -> None: + conf = config.get(DOMAIN) + if conf is None: + return + with cv.prepend_path(DOMAIN): + conf = CONFIG_SCHEMA(conf) + for i, c in enumerate(conf): + with cv.prepend_path(i): + _process_single_config(c) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py new file mode 100644 index 0000000000..6fbaa4e6c9 --- /dev/null +++ b/esphome/components/fingerprint_grow/__init__.py @@ -0,0 +1,293 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome import pins +from esphome.components import uart +from esphome.const import ( + CONF_COLOR, + CONF_COUNT, + CONF_FINGER_ID, + CONF_ID, + CONF_NEW_PASSWORD, + CONF_NUM_SCANS, + CONF_ON_ENROLLMENT_DONE, + CONF_ON_ENROLLMENT_FAILED, + CONF_ON_ENROLLMENT_SCAN, + CONF_ON_FINGER_SCAN_MATCHED, + CONF_ON_FINGER_SCAN_UNMATCHED, + CONF_PASSWORD, + CONF_SENSING_PIN, + CONF_SPEED, + CONF_STATE, + CONF_TRIGGER_ID, +) + +CODEOWNERS = ["@OnFreund", "@loongyh"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "sensor"] +MULTI_CONF = True + +CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id" + +fingerprint_grow_ns = cg.esphome_ns.namespace("fingerprint_grow") +FingerprintGrowComponent = fingerprint_grow_ns.class_( + "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice +) + +FingerScanMatchedTrigger = fingerprint_grow_ns.class_( + "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) +) + +FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( + "FingerScanUnmatchedTrigger", automation.Trigger.template() +) + +EnrollmentScanTrigger = fingerprint_grow_ns.class_( + "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) +) + +EnrollmentDoneTrigger = fingerprint_grow_ns.class_( + "EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16) +) + +EnrollmentFailedTrigger = fingerprint_grow_ns.class_( + "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16) +) + +EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action) +CancelEnrollmentAction = fingerprint_grow_ns.class_( + "CancelEnrollmentAction", automation.Action +) +DeleteAction = fingerprint_grow_ns.class_("DeleteAction", automation.Action) +DeleteAllAction = fingerprint_grow_ns.class_("DeleteAllAction", automation.Action) +LEDControlAction = fingerprint_grow_ns.class_("LEDControlAction", automation.Action) +AuraLEDControlAction = fingerprint_grow_ns.class_( + "AuraLEDControlAction", automation.Action +) + +AuraLEDState = fingerprint_grow_ns.enum("GrowAuraLEDState", True) +AURA_LED_STATES = { + "BREATHING": AuraLEDState.BREATHING, + "FLASHING": AuraLEDState.FLASHING, + "ALWAYS_ON": AuraLEDState.ALWAYS_ON, + "ALWAYS_OFF": AuraLEDState.ALWAYS_OFF, + "GRADUAL_ON": AuraLEDState.GRADUAL_ON, + "GRADUAL_OFF": AuraLEDState.GRADUAL_OFF, +} +validate_aura_led_states = cv.enum(AURA_LED_STATES, upper=True) +AuraLEDColor = fingerprint_grow_ns.enum("GrowAuraLEDColor", True) +AURA_LED_COLORS = { + "RED": AuraLEDColor.RED, + "BLUE": AuraLEDColor.BLUE, + "PURPLE": AuraLEDColor.PURPLE, +} +validate_aura_led_colors = cv.enum(AURA_LED_COLORS, upper=True) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FingerprintGrowComponent), + cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_PASSWORD): cv.uint32_t, + cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, + cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanMatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanUnmatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentScanTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentDoneTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentFailedTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("500ms")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + if CONF_PASSWORD in config: + password = config[CONF_PASSWORD] + cg.add(var.set_password(password)) + yield uart.register_uart_device(var, config) + + if CONF_NEW_PASSWORD in config: + new_password = config[CONF_NEW_PASSWORD] + cg.add(var.set_new_password(new_password)) + + if CONF_SENSING_PIN in config: + sensing_pin = yield cg.gpio_pin_expression(config[CONF_SENSING_PIN]) + cg.add(var.set_sensing_pin(sensing_pin)) + + for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation( + trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf + ) + + for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation( + trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf + ) + + for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + + for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + + +@automation.register_action( + "fingerprint_grow.enroll", + EnrollmentAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t), + cv.Optional(CONF_NUM_SCANS): cv.templatable(cv.uint8_t), + }, + key=CONF_FINGER_ID, + ), +) +def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16) + cg.add(var.set_finger_id(template_)) + if CONF_NUM_SCANS in config: + template_ = yield cg.templatable(config[CONF_NUM_SCANS], args, cg.uint8) + cg.add(var.set_num_scans(template_)) + yield var + + +@automation.register_action( + "fingerprint_grow.cancel_enroll", + CancelEnrollmentAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + } + ), +) +def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action( + "fingerprint_grow.delete", + DeleteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t), + }, + key=CONF_FINGER_ID, + ), +) +def fingerprint_grow_delete_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16) + cg.add(var.set_finger_id(template_)) + yield var + + +@automation.register_action( + "fingerprint_grow.delete_all", + DeleteAllAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + } + ), +) +def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + }, + key=CONF_STATE, +) + + +@automation.register_action( + "fingerprint_grow.led_control", + LEDControlAction, + FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA, +) +def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + template_ = yield cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) + yield var + + +@automation.register_action( + "fingerprint_grow.aura_led_control", + AuraLEDControlAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_STATE): cv.templatable(validate_aura_led_states), + cv.Required(CONF_SPEED): cv.templatable(cv.uint8_t), + cv.Required(CONF_COLOR): cv.templatable(validate_aura_led_colors), + cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t), + } + ), +) +def fingerprint_grow_aura_led_control_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + for key in [CONF_STATE, CONF_SPEED, CONF_COLOR, CONF_COUNT]: + template_ = yield cg.templatable(config[key], args, cg.uint8) + cg.add(getattr(var, f"set_{key}")(template_)) + yield var diff --git a/esphome/components/fingerprint_grow/binary_sensor.py b/esphome/components/fingerprint_grow/binary_sensor.py new file mode 100644 index 0000000000..4f49841f15 --- /dev/null +++ b/esphome/components/fingerprint_grow/binary_sensor.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ICON, ICON_KEY_PLUS +from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent + +DEPENDENCIES = ["fingerprint_grow"] + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), + cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, + } +) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID]) + var = yield binary_sensor.new_binary_sensor(config) + cg.add(hub.set_enrolling_binary_sensor(var)) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp new file mode 100644 index 0000000000..77ddf8ec37 --- /dev/null +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -0,0 +1,434 @@ +#include "fingerprint_grow.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace fingerprint_grow { + +static const char* TAG = "fingerprint_grow"; + +// Based on Adafruit's library: https://github.com/adafruit/Adafruit-Fingerprint-Sensor-Library + +void FingerprintGrowComponent::update() { + if (this->enrollment_image_ > this->enrollment_buffers_) { + this->finish_enrollment(this->save_fingerprint_()); + return; + } + + if (this->sensing_pin_ != nullptr) { + if (this->sensing_pin_->digital_read() == HIGH) { + ESP_LOGV(TAG, "No touch sensing"); + this->waiting_removal_ = false; + return; + } + } + + if (this->waiting_removal_) { + if (this->scan_image_(1) == NO_FINGER) { + ESP_LOGD(TAG, "Finger removed"); + this->waiting_removal_ = false; + } + return; + } + + if (this->enrollment_image_ == 0) { + this->scan_and_match_(); + return; + } + + uint8_t result = this->scan_image_(this->enrollment_image_); + if (result == NO_FINGER) { + return; + } + this->waiting_removal_ = true; + if (result != OK) { + this->finish_enrollment(result); + return; + } + this->enrollment_scan_callback_.call(this->enrollment_image_, this->enrollment_slot_); + ++this->enrollment_image_; +} + +void FingerprintGrowComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); + if (this->check_password_()) { + if (this->new_password_ != nullptr) { + if (this->set_password_()) + return; + } else { + if (this->get_parameters_()) + return; + } + } + this->mark_failed(); +} + +void FingerprintGrowComponent::enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers) { + ESP_LOGI(TAG, "Starting enrollment in slot %d", finger_id); + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(true); + } + this->enrollment_slot_ = finger_id; + this->enrollment_buffers_ = num_buffers; + this->enrollment_image_ = 1; +} + +void FingerprintGrowComponent::finish_enrollment(uint8_t result) { + if (result == OK) { + this->enrollment_done_callback_.call(this->enrollment_slot_); + } else { + this->enrollment_failed_callback_.call(this->enrollment_slot_); + } + this->enrollment_image_ = 0; + this->enrollment_slot_ = 0; + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(false); + } + ESP_LOGI(TAG, "Finished enrollment"); +} + +void FingerprintGrowComponent::scan_and_match_() { + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "Scan and match"); + } else { + ESP_LOGV(TAG, "Scan and match"); + } + if (this->scan_image_(1) == OK) { + this->waiting_removal_ = true; + this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t)(this->capacity_ >> 8), (uint8_t)(this->capacity_ & 0xFF)}; + switch (this->send_command_()) { + case OK: { + ESP_LOGD(TAG, "Fingerprint matched"); + uint16_t finger_id = ((uint16_t) this->data_[1] << 8) | this->data_[2]; + uint16_t confidence = ((uint16_t) this->data_[3] << 8) | this->data_[4]; + if (this->last_finger_id_sensor_ != nullptr) { + this->last_finger_id_sensor_->publish_state(finger_id); + } + if (this->last_confidence_sensor_ != nullptr) { + this->last_confidence_sensor_->publish_state(confidence); + } + this->finger_scan_matched_callback_.call(finger_id, confidence); + break; + } + case NOT_FOUND: + ESP_LOGD(TAG, "Fingerprint not matched to any saved slots"); + this->finger_scan_unmatched_callback_.call(); + break; + } + } +} + +uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "Getting image %d", buffer); + } else { + ESP_LOGV(TAG, "Getting image %d", buffer); + } + this->data_ = {GET_IMAGE}; + switch (this->send_command_()) { + case OK: + break; + case NO_FINGER: + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "No finger"); + } else { + ESP_LOGV(TAG, "No finger"); + } + return this->data_[0]; + case IMAGE_FAIL: + ESP_LOGE(TAG, "Imaging error"); + default: + return this->data_[0]; + } + + ESP_LOGD(TAG, "Processing image %d", buffer); + this->data_ = {IMAGE_2_TZ, buffer}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Processed image %d", buffer); + break; + case IMAGE_MESS: + ESP_LOGE(TAG, "Image too messy"); + break; + case FEATURE_FAIL: + case INVALID_IMAGE: + ESP_LOGE(TAG, "Could not find fingerprint features"); + break; + } + return this->data_[0]; +} + +uint8_t FingerprintGrowComponent::save_fingerprint_() { + ESP_LOGI(TAG, "Creating model"); + this->data_ = {REG_MODEL}; + switch (this->send_command_()) { + case OK: + break; + case ENROLL_MISMATCH: + ESP_LOGE(TAG, "Scans do not match"); + default: + return this->data_[0]; + } + + ESP_LOGI(TAG, "Storing model"); + this->data_ = {STORE, 0x01, (uint8_t)(this->enrollment_slot_ >> 8), (uint8_t)(this->enrollment_slot_ & 0xFF)}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Stored model"); + break; + case BAD_LOCATION: + ESP_LOGE(TAG, "Invalid slot"); + break; + case FLASH_ERR: + ESP_LOGE(TAG, "Error writing to flash"); + break; + } + return this->data_[0]; +} + +bool FingerprintGrowComponent::check_password_() { + ESP_LOGD(TAG, "Checking password"); + this->data_ = {VERIFY_PASSWORD, (uint8_t)(this->password_ >> 24), (uint8_t)(this->password_ >> 16), + (uint8_t)(this->password_ >> 8), (uint8_t)(this->password_ & 0xFF)}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "Password verified"); + return true; + case PASSWORD_FAIL: + ESP_LOGE(TAG, "Wrong password"); + break; + } + return false; +} + +bool FingerprintGrowComponent::set_password_() { + ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_); + this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16), + (uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)}; + if (this->send_command_() == OK) { + ESP_LOGI(TAG, "New password successfully set"); + ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); + ESP_LOGW(TAG, "!!!Forgetting the password will render your device unusable!!!"); + return true; + } + return false; +} + +bool FingerprintGrowComponent::get_parameters_() { + ESP_LOGD(TAG, "Getting parameters"); + this->data_ = {READ_SYS_PARAM}; + if (this->send_command_() == OK) { + ESP_LOGD(TAG, "Got parameters"); + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); + } + this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6]; + if (this->capacity_sensor_ != nullptr) { + this->capacity_sensor_->publish_state(this->capacity_); + } + if (this->security_level_sensor_ != nullptr) { + this->security_level_sensor_->publish_state(((uint16_t) this->data_[7] << 8) | this->data_[8]); + } + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(false); + } + this->get_fingerprint_count_(); + return true; + } + return false; +} + +void FingerprintGrowComponent::get_fingerprint_count_() { + ESP_LOGD(TAG, "Getting fingerprint count"); + this->data_ = {TEMPLATE_COUNT}; + if (this->send_command_() == OK) { + ESP_LOGD(TAG, "Got fingerprint count"); + if (this->fingerprint_count_sensor_ != nullptr) + this->fingerprint_count_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); + } +} + +void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { + ESP_LOGI(TAG, "Deleting fingerprint in slot %d", finger_id); + this->data_ = {DELETE, (uint8_t)(finger_id >> 8), (uint8_t)(finger_id & 0xFF), 0x00, 0x01}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Deleted fingerprint"); + this->get_fingerprint_count_(); + break; + case DELETE_FAIL: + ESP_LOGE(TAG, "Reader failed to delete fingerprint"); + break; + } +} + +void FingerprintGrowComponent::delete_all_fingerprints() { + ESP_LOGI(TAG, "Deleting all stored fingerprints"); + this->data_ = {EMPTY}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Deleted all fingerprints"); + this->get_fingerprint_count_(); + break; + case DB_CLEAR_FAIL: + ESP_LOGE(TAG, "Reader failed to clear fingerprint library"); + break; + } +} + +void FingerprintGrowComponent::led_control(bool state) { + ESP_LOGD(TAG, "Setting LED"); + if (state) + this->data_ = {LED_ON}; + else + this->data_ = {LED_OFF}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "LED set"); + break; + case PACKET_RCV_ERR: + case TIMEOUT: + break; + default: + ESP_LOGE(TAG, "Try aura_led_control instead"); + break; + } +} + +void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count) { + const uint32_t now = millis(); + const uint32_t elapsed = now - this->last_aura_led_control_; + if (elapsed < this->last_aura_led_duration_) { + delay(this->last_aura_led_duration_ - elapsed); + } + ESP_LOGD(TAG, "Setting Aura LED"); + this->data_ = {AURA_CONFIG, state, speed, color, count}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "Aura LED set"); + this->last_aura_led_control_ = millis(); + this->last_aura_led_duration_ = 10 * speed * count; + break; + case PACKET_RCV_ERR: + case TIMEOUT: + break; + default: + ESP_LOGE(TAG, "Try led_control instead"); + break; + } +} + +uint8_t FingerprintGrowComponent::send_command_() { + this->write((uint8_t)(START_CODE >> 8)); + this->write((uint8_t)(START_CODE & 0xFF)); + this->write(this->address_[0]); + this->write(this->address_[1]); + this->write(this->address_[2]); + this->write(this->address_[3]); + this->write(COMMAND); + + uint16_t wire_length = this->data_.size() + 2; + this->write((uint8_t)(wire_length >> 8)); + this->write((uint8_t)(wire_length & 0xFF)); + + uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; + for (auto data : this->data_) { + this->write(data); + sum += data; + } + + this->write((uint8_t)(sum >> 8)); + this->write((uint8_t)(sum & 0xFF)); + + this->data_.clear(); + + uint8_t byte; + uint16_t idx = 0, length = 0; + + for (uint16_t timer = 0; timer < 1000; timer++) { + if (this->available() == 0) { + delay(1); + continue; + } + byte = this->read(); + switch (idx) { + case 0: + if (byte != (uint8_t)(START_CODE >> 8)) + continue; + break; + case 1: + if (byte != (uint8_t)(START_CODE & 0xFF)) { + idx = 0; + continue; + } + break; + case 2: + case 3: + case 4: + case 5: + if (byte != this->address_[idx - 2]) { + idx = 0; + continue; + } + break; + case 6: + if (byte != ACK) { + idx = 0; + continue; + } + break; + case 7: + length = (uint16_t) byte << 8; + break; + case 8: + length |= byte; + break; + default: + this->data_.push_back(byte); + if ((idx - 8) == length) { + switch (this->data_[0]) { + case OK: + case NO_FINGER: + case IMAGE_FAIL: + case IMAGE_MESS: + case FEATURE_FAIL: + case NO_MATCH: + case NOT_FOUND: + case ENROLL_MISMATCH: + case BAD_LOCATION: + case DELETE_FAIL: + case DB_CLEAR_FAIL: + case PASSWORD_FAIL: + case INVALID_IMAGE: + case FLASH_ERR: + break; + case PACKET_RCV_ERR: + ESP_LOGE(TAG, "Reader failed to process request"); + break; + default: + ESP_LOGE(TAG, "Unknown response received from reader: %d", this->data_[0]); + break; + } + return this->data_[0]; + } + break; + } + idx++; + } + ESP_LOGE(TAG, "No response received from reader"); + this->data_[0] = TIMEOUT; + return TIMEOUT; +} + +void FingerprintGrowComponent::dump_config() { + ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); + LOG_SENSOR(" ", "Status", this->status_sensor_); + LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); + LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); + LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); + LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); +} + +} // namespace fingerprint_grow +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h new file mode 100644 index 0000000000..e7d734777a --- /dev/null +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -0,0 +1,276 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace fingerprint_grow { + +static const uint16_t START_CODE = 0xEF01; + +enum GrowPacketType { + COMMAND = 0x01, + DATA = 0x02, + ACK = 0x07, + END_DATA = 0x08, +}; + +enum GrowCommand { + GET_IMAGE = 0x01, + IMAGE_2_TZ = 0x02, + SEARCH = 0x04, + REG_MODEL = 0x05, + STORE = 0x06, + LOAD = 0x07, + UPLOAD = 0x08, + DELETE = 0x0C, + EMPTY = 0x0D, + READ_SYS_PARAM = 0x0F, + SET_PASSWORD = 0x12, + VERIFY_PASSWORD = 0x13, + HI_SPEED_SEARCH = 0x1B, + TEMPLATE_COUNT = 0x1D, + AURA_CONFIG = 0x35, + LED_ON = 0x50, + LED_OFF = 0x51, +}; + +enum GrowResponse { + OK = 0x00, + PACKET_RCV_ERR = 0x01, + NO_FINGER = 0x02, + IMAGE_FAIL = 0x03, + IMAGE_MESS = 0x06, + FEATURE_FAIL = 0x07, + NO_MATCH = 0x08, + NOT_FOUND = 0x09, + ENROLL_MISMATCH = 0x0A, + BAD_LOCATION = 0x0B, + DB_RANGE_FAIL = 0x0C, + UPLOAD_FEATURE_FAIL = 0x0D, + PACKET_RESPONSE_FAIL = 0x0E, + UPLOAD_FAIL = 0x0F, + DELETE_FAIL = 0x10, + DB_CLEAR_FAIL = 0x11, + PASSWORD_FAIL = 0x13, + INVALID_IMAGE = 0x15, + FLASH_ERR = 0x18, + INVALID_REG = 0x1A, + BAD_PACKET = 0xFE, + TIMEOUT = 0xFF, +}; + +enum GrowAuraLEDState { + BREATHING = 0x01, + FLASHING = 0x02, + ALWAYS_ON = 0x03, + ALWAYS_OFF = 0x04, + GRADUAL_ON = 0x05, + GRADUAL_OFF = 0x06, +}; + +enum GrowAuraLEDColor { + RED = 0x01, + BLUE = 0x02, + PURPLE = 0x03, +}; + +class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevice { + public: + void update() override; + void setup() override; + void dump_config() override; + + void set_address(uint32_t address) { + this->address_[0] = (uint8_t)(address >> 24); + this->address_[1] = (uint8_t)(address >> 16); + this->address_[2] = (uint8_t)(address >> 8); + this->address_[3] = (uint8_t)(address & 0xFF); + } + void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } + void set_password(uint32_t password) { this->password_ = password; } + void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; } + void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { + this->fingerprint_count_sensor_ = fingerprint_count_sensor; + } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_capacity_sensor(sensor::Sensor *capacity_sensor) { this->capacity_sensor_ = capacity_sensor; } + void set_security_level_sensor(sensor::Sensor *security_level_sensor) { + this->security_level_sensor_ = security_level_sensor; + } + void set_last_finger_id_sensor(sensor::Sensor *last_finger_id_sensor) { + this->last_finger_id_sensor_ = last_finger_id_sensor; + } + void set_last_confidence_sensor(sensor::Sensor *last_confidence_sensor) { + this->last_confidence_sensor_ = last_confidence_sensor; + } + void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { + this->enrolling_binary_sensor_ = enrolling_binary_sensor; + } + void add_on_finger_scan_matched_callback(std::function callback) { + this->finger_scan_matched_callback_.add(std::move(callback)); + } + void add_on_finger_scan_unmatched_callback(std::function callback) { + this->finger_scan_unmatched_callback_.add(std::move(callback)); + } + void add_on_enrollment_scan_callback(std::function callback) { + this->enrollment_scan_callback_.add(std::move(callback)); + } + void add_on_enrollment_done_callback(std::function callback) { + this->enrollment_done_callback_.add(std::move(callback)); + } + + void add_on_enrollment_failed_callback(std::function callback) { + this->enrollment_failed_callback_.add(std::move(callback)); + } + + void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers); + void finish_enrollment(uint8_t result); + void delete_fingerprint(uint16_t finger_id); + void delete_all_fingerprints(); + + void led_control(bool state); + void aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count); + + protected: + void scan_and_match_(); + uint8_t scan_image_(uint8_t buffer); + uint8_t save_fingerprint_(); + bool check_password_(); + bool set_password_(); + bool get_parameters_(); + void get_fingerprint_count_(); + uint8_t send_command_(); + + std::vector data_ = {}; + uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + uint16_t capacity_ = 64; + uint32_t password_ = 0x0; + uint32_t *new_password_{nullptr}; + GPIOPin *sensing_pin_{nullptr}; + uint8_t enrollment_image_ = 0; + uint16_t enrollment_slot_ = 0; + uint8_t enrollment_buffers_ = 5; + bool waiting_removal_ = false; + uint32_t last_aura_led_control_ = 0; + uint16_t last_aura_led_duration_ = 0; + sensor::Sensor *fingerprint_count_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + sensor::Sensor *capacity_sensor_{nullptr}; + sensor::Sensor *security_level_sensor_{nullptr}; + sensor::Sensor *last_finger_id_sensor_{nullptr}; + sensor::Sensor *last_confidence_sensor_{nullptr}; + binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + CallbackManager finger_scan_matched_callback_; + CallbackManager finger_scan_unmatched_callback_; + CallbackManager enrollment_scan_callback_; + CallbackManager enrollment_done_callback_; + CallbackManager enrollment_failed_callback_; +}; + +class FingerScanMatchedTrigger : public Trigger { + public: + explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_matched_callback( + [this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); }); + } +}; + +class FingerScanUnmatchedTrigger : public Trigger<> { + public: + explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); }); + } +}; + +class EnrollmentScanTrigger : public Trigger { + public: + explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_scan_callback( + [this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); }); + } +}; + +class EnrollmentDoneTrigger : public Trigger { + public: + explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); + } +}; + +class EnrollmentFailedTrigger : public Trigger { + public: + explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); + } +}; + +template class EnrollmentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, finger_id) + TEMPLATABLE_VALUE(uint8_t, num_scans) + + void play(Ts... x) override { + auto finger_id = this->finger_id_.value(x...); + auto num_scans = this->num_scans_.value(x...); + if (num_scans) { + this->parent_->enroll_fingerprint(finger_id, num_scans); + } else { + this->parent_->enroll_fingerprint(finger_id, 2); + } + } +}; + +template +class CancelEnrollmentAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->finish_enrollment(1); } +}; + +template class DeleteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, finger_id) + + void play(Ts... x) override { + auto finger_id = this->finger_id_.value(x...); + this->parent_->delete_fingerprint(finger_id); + } +}; + +template class DeleteAllAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->delete_all_fingerprints(); } +}; + +template class LEDControlAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + auto state = this->state_.value(x...); + this->parent_->led_control(state); + } +}; + +template class AuraLEDControlAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, state) + TEMPLATABLE_VALUE(uint8_t, speed) + TEMPLATABLE_VALUE(uint8_t, color) + TEMPLATABLE_VALUE(uint8_t, count) + + void play(Ts... x) override { + auto state = this->state_.value(x...); + auto speed = this->speed_.value(x...); + auto color = this->color_.value(x...); + auto count = this->count_.value(x...); + + this->parent_->aura_led_control(state, speed, color, count); + } +}; + +} // namespace fingerprint_grow +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py new file mode 100644 index 0000000000..c76c898727 --- /dev/null +++ b/esphome/components/fingerprint_grow/sensor.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_CAPACITY, + CONF_FINGERPRINT_COUNT, + CONF_LAST_CONFIDENCE, + CONF_LAST_FINGER_ID, + CONF_SECURITY_LEVEL, + CONF_STATUS, + DEVICE_CLASS_EMPTY, + ICON_ACCOUNT, + ICON_ACCOUNT_CHECK, + ICON_DATABASE, + ICON_EMPTY, + ICON_FINGERPRINT, + ICON_SECURITY, + UNIT_EMPTY, +) +from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent + +DEPENDENCIES = ["fingerprint_grow"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), + cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( + UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_CAPACITY): sensor.sensor_schema( + UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( + UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY + ), + } +) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID]) + + for key in [ + CONF_FINGERPRINT_COUNT, + CONF_STATUS, + CONF_CAPACITY, + CONF_SECURITY_LEVEL, + CONF_LAST_FINGER_ID, + CONF_LAST_CONFIDENCE, + ]: + if key not in config: + continue + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 75ee3f708b..2676609d9b 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -41,33 +41,33 @@ const uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16; // Power on const uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17; -const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01; const uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00; +const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01; // Mode const uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19; const uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00; -const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04; const uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01; const uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02; const uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03; +const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04; // const uint8_t FUJITSU_GENERAL_MODE_10C = 0x0B; // Swing -const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 20; +const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20; +const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00; +const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01; +const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02; +const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03; + +// Fan +const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21; const uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00; const uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01; const uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02; const uint8_t FUJITSU_GENERAL_FAN_LOW = 0x03; const uint8_t FUJITSU_GENERAL_FAN_SILENT = 0x04; -// Fan speed -const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 21; -const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00; -const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01; -const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02; -const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03; - // TODO Outdoor Unit Low Noise // const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; // const uint8_t FUJITSU_GENERAL_STATE_BYTE14 = 0x20; diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 8154d7a1d2..e97615f739 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -11,6 +11,42 @@ namespace fujitsu_general { const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius +// clang-format off +/** + * ``` + * turn + * on temp mode fan swing + * * | | | | | | * + * + * temperatures 1 1248 124 124 1 + * auto auto 18 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000100 00000000 00000000 00000000 00000000 00000000 00000100 11110001 + * auto auto 19 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10001100 00000000 00000000 00000000 00000000 00000000 00000100 11111110 + * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 + * + * on flag: + * on at 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000000 00100000 00000000 00000000 00000000 00000000 00000100 11010101 + * down to 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000000 00100000 00000000 00000000 00000000 00000000 00000100 00110101 + * + * mode options: + * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 + * cool auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 10000000 00000000 00000000 00000000 00000000 00000100 01110011 + * dry auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 01000000 00000000 00000000 00000000 00000000 00000100 10110011 + * fan (auto) (30) 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 11000000 00000000 00000000 00000000 00000000 00000100 00110011 + * heat auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 00000000 00000000 00000000 00000000 00000100 11010011 + * + * fan options: + * heat 30 high 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 10000000 00000000 00000000 00000000 00000100 01010011 + * heat 30 med 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 01000000 00000000 00000000 00000000 00000100 01010011 + * heat 30 low 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 11000000 00000000 00000000 00000000 00000100 10010011 + * heat 30 quiet 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 + * + * swing options: + * heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101 + * heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 + * ``` + */ +// clang-format on + class FujitsuGeneralClimate : public climate_ir::ClimateIR { public: FujitsuGeneralClimate() diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 60dcc2002c..c09a49315c 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -1,9 +1,27 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_SPEED, + CONF_COURSE, + CONF_ALTITUDE, + CONF_SATELLITES, + UNIT_DEGREES, + UNIT_KILOMETER_PER_HOUR, + UNIT_METER, + UNIT_EMPTY, + ICON_EMPTY, + DEVICE_CLASS_EMPTY, +) DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor"] + +CODEOWNERS = ["@coogle"] gps_ns = cg.esphome_ns.namespace("gps") GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) @@ -15,9 +33,27 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(GPS), + cv.Optional(CONF_LATITUDE): sensor.sensor_schema( + UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( + UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + UNIT_KILOMETER_PER_HOUR, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_COURSE): sensor.sensor_schema( + UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( + UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY + ), } ) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("20s")) .extend(uart.UART_DEVICE_SCHEMA) ) @@ -27,5 +63,29 @@ def to_code(config): yield cg.register_component(var, config) yield uart.register_uart_device(var, config) + if CONF_LATITUDE in config: + sens = yield sensor.new_sensor(config[CONF_LATITUDE]) + cg.add(var.set_latitude_sensor(sens)) + + if CONF_LONGITUDE in config: + sens = yield sensor.new_sensor(config[CONF_LONGITUDE]) + cg.add(var.set_longitude_sensor(sens)) + + if CONF_SPEED in config: + sens = yield sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_COURSE in config: + sens = yield sensor.new_sensor(config[CONF_COURSE]) + cg.add(var.set_course_sensor(sens)) + + if CONF_ALTITUDE in config: + sens = yield sensor.new_sensor(config[CONF_ALTITUDE]) + cg.add(var.set_altitude_sensor(sens)) + + if CONF_SATELLITES in config: + sens = yield sensor.new_sensor(config[CONF_SATELLITES]) + cg.add(var.set_satellites_sensor(sens)) + # https://platformio.org/lib/show/1655/TinyGPSPlus cg.add_library("1655", "1.0.2") # TinyGPSPlus, has name conflict diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 26371565f3..ba0afdf7cc 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -8,34 +8,57 @@ static const char *TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::update() { + if (this->latitude_sensor_ != nullptr) + this->latitude_sensor_->publish_state(this->latitude_); + + if (this->longitude_sensor_ != nullptr) + this->longitude_sensor_->publish_state(this->longitude_); + + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(this->speed_); + + if (this->course_sensor_ != nullptr) + this->course_sensor_->publish_state(this->course_); + + if (this->altitude_sensor_ != nullptr) + this->altitude_sensor_->publish_state(this->altitude_); + + if (this->satellites_sensor_ != nullptr) + this->satellites_sensor_->publish_state(this->satellites_); +} + void GPS::loop() { while (this->available() && !this->has_time_) { if (this->tiny_gps_.encode(this->read())) { if (tiny_gps_.location.isUpdated()) { + this->latitude_ = tiny_gps_.location.lat(); + this->longitude_ = tiny_gps_.location.lng(); + ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %f", tiny_gps_.location.lat()); - ESP_LOGD(TAG, " Lon: %f", tiny_gps_.location.lng()); + ESP_LOGD(TAG, " Lat: %f", this->latitude_); + ESP_LOGD(TAG, " Lon: %f", this->longitude_); } if (tiny_gps_.speed.isUpdated()) { + this->speed_ = tiny_gps_.speed.kmph(); ESP_LOGD(TAG, "Speed:"); - ESP_LOGD(TAG, " %f km/h", tiny_gps_.speed.kmph()); + ESP_LOGD(TAG, " %f km/h", this->speed_); } if (tiny_gps_.course.isUpdated()) { + this->course_ = tiny_gps_.course.deg(); ESP_LOGD(TAG, "Course:"); - ESP_LOGD(TAG, " %f °", tiny_gps_.course.deg()); + ESP_LOGD(TAG, " %f °", this->course_); } if (tiny_gps_.altitude.isUpdated()) { + this->altitude_ = tiny_gps_.altitude.meters(); ESP_LOGD(TAG, "Altitude:"); - ESP_LOGD(TAG, " %f m", tiny_gps_.altitude.meters()); + ESP_LOGD(TAG, " %f m", this->altitude_); } if (tiny_gps_.satellites.isUpdated()) { + this->satellites_ = tiny_gps_.satellites.value(); ESP_LOGD(TAG, "Satellites:"); - ESP_LOGD(TAG, " %d", tiny_gps_.satellites.value()); - } - if (tiny_gps_.satellites.isUpdated()) { - ESP_LOGD(TAG, "HDOP:"); - ESP_LOGD(TAG, " %.2f", tiny_gps_.hdop.hdop()); + ESP_LOGD(TAG, " %d", this->satellites_); } for (auto *listener : this->listeners_) diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 84a9248bc6..50dd476ae3 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" #include namespace esphome { @@ -20,17 +21,41 @@ class GPSListener { GPS *parent_; }; -class GPS : public Component, public uart::UARTDevice { +class GPS : public PollingComponent, public uart::UARTDevice { public: + void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; } + void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; } + void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; } + void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } + void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } + void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } + void register_listener(GPSListener *listener) { listener->parent_ = this; this->listeners_.push_back(listener); } float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + void update() override; + TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: + float latitude_ = -1; + float longitude_ = -1; + float speed_ = -1; + float course_ = -1; + float altitude_ = -1; + int satellites_ = -1; + + sensor::Sensor *latitude_sensor_{nullptr}; + sensor::Sensor *longitude_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *course_sensor_{nullptr}; + sensor::Sensor *altitude_sensor_{nullptr}; + sensor::Sensor *satellites_sensor_{nullptr}; + bool has_time_{false}; TinyGPSPlus tiny_gps_; std::vector listeners_{}; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index dee3fe8f77..ef664a9d35 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, Lambda -from esphome.core_config import PLATFORMIO_ESP8266_LUT +from esphome.core.config import PLATFORMIO_ESP8266_LUT DEPENDENCIES = ["network"] AUTO_LOAD = ["json"] diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 1446835904..59f90842e1 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( + CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, CONF_SCAN, @@ -9,6 +10,7 @@ from esphome.const import ( CONF_SDA, CONF_ADDRESS, CONF_I2C_ID, + CONF_MULTIPLEXER, ) from esphome.core import coroutine, coroutine_with_priority @@ -16,6 +18,7 @@ CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CComponent = i2c_ns.class_("I2CComponent", cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") +I2CMultiplexer = i2c_ns.class_("I2CMultiplexer", I2CDevice) MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( @@ -30,6 +33,13 @@ CONFIG_SCHEMA = cv.Schema( } ).extend(cv.COMPONENT_SCHEMA) +I2CMULTIPLEXER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(I2CMultiplexer), + cv.Required(CONF_CHANNEL): cv.uint8_t, + } +) + @coroutine_with_priority(1.0) def to_code(config): @@ -53,6 +63,7 @@ def i2c_device_schema(default_address): """ schema = { cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CComponent), + cv.Optional(CONF_MULTIPLEXER): I2CMULTIPLEXER_SCHEMA, } if default_address is None: schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address @@ -72,3 +83,8 @@ def register_i2c_device(var, config): parent = yield cg.get_variable(config[CONF_I2C_ID]) cg.add(var.set_i2c_parent(parent)) cg.add(var.set_i2c_address(config[CONF_ADDRESS])) + if CONF_MULTIPLEXER in config: + multiplexer = yield cg.get_variable(config[CONF_MULTIPLEXER][CONF_ID]) + cg.add( + var.set_i2c_multiplexer(multiplexer, config[CONF_MULTIPLEXER][CONF_CHANNEL]) + ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 8e6d9f32fa..e9a7e72932 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -178,28 +178,67 @@ bool I2CComponent::write_byte_16(uint8_t address, uint8_t a_register, uint16_t d } void I2CDevice::set_i2c_address(uint8_t address) { this->address_ = address; } +#ifdef USE_I2C_MULTIPLEXER +void I2CDevice::set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel) { + ESP_LOGVV(TAG, " Setting Multiplexer %p for channel %d", multiplexer, channel); + this->multiplexer_ = multiplexer; + this->channel_ = channel; +} + +void I2CDevice::check_multiplexer_() { + if (this->multiplexer_ != nullptr) { + ESP_LOGVV(TAG, "Multiplexer setting channel to %d", this->channel_); + this->multiplexer_->set_channel(this->channel_); + } +} +#endif + bool I2CDevice::read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_bytes(this->address_, a_register, data, len, conversion); } bool I2CDevice::read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_byte(this->address_, a_register, data, conversion); } bool I2CDevice::write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_bytes(this->address_, a_register, data, len); } bool I2CDevice::write_byte(uint8_t a_register, uint8_t data) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_byte(this->address_, a_register, data); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_bytes_16(this->address_, a_register, data, len, conversion); } bool I2CDevice::read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_byte_16(this->address_, a_register, data, conversion); } bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_bytes_16(this->address_, a_register, data, len); } bool I2CDevice::write_byte_16(uint8_t a_register, uint16_t data) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_byte_16(this->address_, a_register, data); } void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; } diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 72777f8eb0..56da64c218 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,6 +1,7 @@ #pragma once #include +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -135,7 +136,7 @@ extern uint8_t next_i2c_bus_num_; #endif class I2CDevice; - +class I2CMultiplexer; class I2CRegister { public: I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} @@ -167,7 +168,10 @@ class I2CDevice { /// Manually set the i2c address of this device. void set_i2c_address(uint8_t address); - +#ifdef USE_I2C_MULTIPLEXER + /// Manually set the i2c multiplexer of this device. + void set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel); +#endif /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); @@ -280,9 +284,19 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data); protected: + // Checks for multiplexer set and set channel + void check_multiplexer_(); uint8_t address_{0x00}; I2CComponent *parent_{nullptr}; +#ifdef USE_I2C_MULTIPLEXER + I2CMultiplexer *multiplexer_{nullptr}; + uint8_t channel_; +#endif +}; +class I2CMultiplexer : public I2CDevice { + public: + I2CMultiplexer() = default; + virtual void set_channel(uint8_t channelno); }; - } // namespace i2c } // namespace esphome diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index d6d930e9d4..775ee363af 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -11,6 +11,40 @@ inline static float random_cubic_float() { return r * r * r; } +/// Pulse effect. +class PulseLightEffect : public LightEffect { + public: + explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} + + void apply() override { + const uint32_t now = millis(); + if (now - this->last_color_change_ < this->update_interval_) { + return; + } + auto call = this->state_->turn_on(); + float out = this->on_ ? 1.0 : 0.0; + call.set_brightness_if_supported(out); + this->on_ = !this->on_; + call.set_transition_length_if_supported(this->transition_length_); + // don't tell HA every change + call.set_publish(false); + call.set_save(false); + call.perform(); + + this->last_color_change_ = now; + } + + void set_transition_length(uint32_t transition_length) { this->transition_length_ = transition_length; } + + void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } + + protected: + bool on_ = false; + uint32_t last_color_change_{0}; + uint32_t transition_length_{}; + uint32_t update_interval_{}; +}; + /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. class RandomLightEffect : public LightEffect { public: @@ -22,10 +56,14 @@ class RandomLightEffect : public LightEffect { return; } auto call = this->state_->turn_on(); - call.set_red_if_supported(random_float()); - call.set_green_if_supported(random_float()); - call.set_blue_if_supported(random_float()); - call.set_white_if_supported(random_float()); + if (this->state_->get_traits().get_supports_rgb()) { + call.set_red_if_supported(random_float()); + call.set_green_if_supported(random_float()); + call.set_blue_if_supported(random_float()); + call.set_white_if_supported(random_float()); + } else { + call.set_brightness_if_supported(random_float()); + } call.set_color_temperature_if_supported(random_float()); call.set_transition_length_if_supported(this->transition_length_); call.set_publish(true); diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 9f017de98b..08a78d90ed 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -26,6 +26,7 @@ from esphome.const import ( from esphome.util import Registry from .types import ( LambdaLightEffect, + PulseLightEffect, RandomLightEffect, StrobeLightEffect, StrobeLightEffectColor, @@ -152,7 +153,27 @@ def automation_effect_to_code(config, effect_id): yield var -@register_rgb_effect( +@register_monochromatic_effect( + "pulse", + PulseLightEffect, + "Pulse", + { + cv.Optional( + CONF_TRANSITION_LENGTH, default="1s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_UPDATE_INTERVAL, default="1s" + ): cv.positive_time_period_milliseconds, + }, +) +def pulse_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) + cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) + yield effect + + +@register_monochromatic_effect( "random", RandomLightEffect, "Random", diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 4bca266b67..7c96cda7b1 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -31,6 +31,7 @@ LightTurnOffTrigger = light_ns.class_( # Effects LightEffect = light_ns.class_("LightEffect") +PulseLightEffect = light_ns.class_("PulseLightEffect", LightEffect) RandomLightEffect = light_ns.class_("RandomLightEffect", LightEffect) LambdaLightEffect = light_ns.class_("LambdaLightEffect", LightEffect) AutomationLightEffect = light_ns.class_("AutomationLightEffect", LightEffect) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 140b8f26c1..8e7bd2ee49 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -136,7 +136,11 @@ void Logger::pre_setup() { break; #ifdef ARDUINO_ARCH_ESP32 case UART_SELECTION_UART2: +#if !CONFIG_IDF_TARGET_ESP32S2 + // FIXME: Validate in config that UART2 can't be set for ESP32-S2 (only has + // UART0-UART1) this->hw_serial_ = &Serial2; +#endif break; #endif } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 2eb1c52153..b8743fc142 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -25,9 +25,17 @@ void MQTTClientComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up MQTT..."); this->mqtt_client_.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - std::string payload_s(payload, len); - std::string topic_s(topic); - this->on_message(topic_s, payload_s); + if (index == 0) + this->payload_buffer_.reserve(total); + + // append new payload, may contain incomplete MQTT message + this->payload_buffer_.append(payload, len); + + // MQTT fully received + if (len + index == total) { + this->on_message(topic, this->payload_buffer_); + this->payload_buffer_.clear(); + } }); this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { this->state_ = MQTT_CLIENT_DISCONNECTED; @@ -347,6 +355,26 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, mqtt_json_cal this->subscriptions_.push_back(subscription); } +void MQTTClientComponent::unsubscribe(const std::string &topic) { + uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str()); + yield(); + if (ret != 0) { + ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str()); + } else { + delay(5); + ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str()); + this->status_momentary_warning("unsubscribe", 1000); + } + + auto it = subscriptions_.begin(); + while (it != subscriptions_.end()) { + if (it->topic == topic) + it = subscriptions_.erase(it); + else + ++it; + } +} + // Publish bool MQTTClientComponent::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) { return this->publish(topic, payload.data(), payload.size(), qos, retain); diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 2bbebff845..e4f7c479b2 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -159,6 +159,15 @@ class MQTTClientComponent : public Component { */ void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0); + /** Unsubscribe from an MQTT topic. + * + * If multiple existing subscriptions to the same topic exist, all of them will be removed. + * + * @param topic The topic to unsubscribe from. + * Must match the topic in the original subscribe or subscribe_json call exactly. + */ + void unsubscribe(const std::string &topic); + /** Publish a MQTTMessage * * @param message The message. @@ -250,6 +259,7 @@ class MQTTClientComponent : public Component { }; std::string topic_prefix_{}; MQTTMessage log_message_; + std::string payload_buffer_; int log_level_{ESPHOME_LOG_LEVEL}; std::vector subscriptions_; diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 46713d3ffe..5581a2b8d3 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,2 +1,13 @@ # Dummy package to allow components to depend on network +import esphome.codegen as cg +from esphome.core import CORE + CODEOWNERS = ["@esphome/core"] + + +def add_mdns_library(): + cg.add_define("USE_MDNS") + if CORE.is_esp32: + cg.add_library("ESPmDNS", None) + elif CORE.is_esp8266: + cg.add_library("ESP8266mDNS", None) diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py index 970b867e79..7858213f06 100644 --- a/esphome/components/rc522/__init__.py +++ b/esphome/components/rc522/__init__.py @@ -7,7 +7,6 @@ from esphome.core import coroutine CODEOWNERS = ["@glmnet"] AUTO_LOAD = ["binary_sensor"] -MULTI_CONF = True CONF_RC522_ID = "rc522_id" diff --git a/esphome/components/rc522_i2c/__init__.py b/esphome/components/rc522_i2c/__init__.py index 532adfce79..081536b6b1 100644 --- a/esphome/components/rc522_i2c/__init__.py +++ b/esphome/components/rc522_i2c/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID CODEOWNERS = ["@glmnet"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["rc522"] - +MULTI_CONF = True rc522_i2c_ns = cg.esphome_ns.namespace("rc522_i2c") RC522I2C = rc522_i2c_ns.class_("RC522I2C", rc522.RC522, i2c.I2CDevice) diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py index 6ae163bcb4..2e5630f46d 100644 --- a/esphome/components/rc522_spi/__init__.py +++ b/esphome/components/rc522_spi/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID CODEOWNERS = ["@glmnet"] DEPENDENCIES = ["spi"] AUTO_LOAD = ["rc522"] +MULTI_CONF = True rc522_spi_ns = cg.esphome_ns.namespace("rc522_spi") RC522Spi = rc522_spi_ns.class_("RC522Spi", rc522.RC522, spi.SPIDevice) diff --git a/esphome/components/sgp40/__init__.py b/esphome/components/sgp40/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.cpp b/esphome/components/sgp40/sensirion_voc_algorithm.cpp new file mode 100644 index 0000000000..c82db2301a --- /dev/null +++ b/esphome/components/sgp40/sensirion_voc_algorithm.cpp @@ -0,0 +1,629 @@ + +#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) + 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 diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.h b/esphome/components/sgp40/sensirion_voc_algorithm.h new file mode 100644 index 0000000000..05431635ad --- /dev/null +++ b/esphome/components/sgp40/sensirion_voc_algorithm.h @@ -0,0 +1,147 @@ +#pragma once +#include +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 diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py new file mode 100644 index 0000000000..40bc07389b --- /dev/null +++ b/esphome/components/sgp40/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, ICON_RADIATOR, UNIT_EMPTY + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@SenexCrenshaw"] + +sgp40_ns = cg.esphome_ns.namespace("sgp40") +SGP40Component = sgp40_ns.class_( + "SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONF_COMPENSATION = "compensation" +CONF_HUMIDITY_SOURCE = "humidity_source" +CONF_TEMPERATURE_SOURCE = "temperature_source" +CONF_STORE_BASELINE = "store_baseline" +CONF_VOC_BASELINE = "voc_baseline" + +CONFIG_SCHEMA = ( + sensor.sensor_schema(UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(SGP40Component), + 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)) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield sensor.register_sensor(var, config) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = yield 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)) diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp new file mode 100644 index 0000000000..3e9f2b96cf --- /dev/null +++ b/esphome/components/sgp40/sgp40.cpp @@ -0,0 +1,314 @@ +#include "esphome/core/log.h" +#include "sgp40.h" + +namespace esphome { +namespace sgp40 { + +static const char *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: %llu", 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[1]; + if (!this->read_data_(raw_featureset, 1)) { + ESP_LOGD(TAG, "raw_featureset read_data_ failed"); + this->mark_failed(); + return; + } + + this->featureset_ = raw_featureset[0]; + 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(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_(); +} + +void SGP40Component::self_test_() { + ESP_LOGD(TAG, "selfTest started"); + if (!this->write_command_(SGP40_CMD_SELF_TEST)) { + this->error_code_ = COMMUNICATION_FAILED; + ESP_LOGD(TAG, "selfTest communicatin failed"); + this->mark_failed(); + } + + this->set_timeout(250, [this]() { + uint16_t reply[1]; + if (!this->read_data_(reply, 1)) { + ESP_LOGD(TAG, "selfTest read_data_ failed"); + this->mark_failed(); + return; + } + + if (reply[0] == 0xD400) { + ESP_LOGD(TAG, "selfTest completed"); + return; + } + + ESP_LOGD(TAG, "selfTest 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 (abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF || + 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->humidity_sensor_ != nullptr) { + humidity = this->humidity_sensor_->state; + } + if (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 (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + temperature = 25; + } + + uint8_t command[8]; + + command[0] = 0x26; + command[1] = 0x0F; + + uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); + command[2] = rhticks >> 8; + command[3] = rhticks & 0xFF; + command[4] = generate_crc_(command + 2, 2); + uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); + command[5] = tempticks >> 8; + command[6] = tempticks & 0xFF; + command[7] = generate_crc_(command + 5, 2); + + if (!this->write_bytes_raw(command, 8)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write_bytes_raw error"); + return UINT16_MAX; + } + delay(250); // NOLINT + uint16_t raw_data[1]; + + if (!this->read_data_(raw_data, 1)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read_data_ error"); + return UINT16_MAX; + } + return raw_data[0]; +} + +uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) { + // calculates 8-Bit checksum with given polynomial + uint8_t crc = SGP40_CRC8_INIT; + + for (uint8_t i = 0; i < datalen; i++) { + crc ^= data[i]; + for (uint8_t b = 0; b < 8; b++) { + if (crc & 0x80) + crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL; + else + crc <<= 1; + } + } + return crc; +} + +void SGP40Component::update() { + this->seconds_since_last_store_ += this->update_interval_ / 1000; + + uint32_t voc_index = this->measure_voc_index_(); + + if (this->samples_read_++ < this->samples_to_stabalize_) { + ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, + this->samples_to_stabalize_, voc_index); + return; + } + + if (voc_index != UINT16_MAX) { + this->status_clear_warning(); + this->publish_state(voc_index); + } else { + this->status_set_warning(); + } +} + +void SGP40Component::dump_config() { + ESP_LOGCONFIG(TAG, "SGP40:"); + LOG_I2C_DEVICE(this); + 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: %llu", 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"); + } +} + +bool SGP40Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SGP40Component::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SGP40Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + std::vector buf(num_bytes); + + if (!this->parent_->raw_receive(this->address_, buf.data(), num_bytes)) { + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + return true; +} + +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h new file mode 100644 index 0000000000..d448b5e45c --- /dev/null +++ b/esphome/components/sgp40/sgp40.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/application.h" +#include "esphome/core/preferences.h" +#include "sensirion_voc_algorithm.h" + +#include + +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; + +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const long SHORTEST_BASELINE_STORE_INTERVAL = 10800; + +// Store anyway if the baseline difference exceeds the max storage diff value +const long 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 i2c::I2CDevice { + 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 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}; + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + int16_t sensirion_init_sensors_(); + int16_t sgp40_probe_(); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + 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_; + long seconds_since_last_store_; + SGP40Baselines baselines_storage_; + VocAlgorithmParams voc_algorithm_params_; + bool store_baseline_; + int32_t state0_; + int32_t state1_; + uint8_t samples_read_ = 0; + uint8_t samples_to_stabalize_ = static_cast(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 diff --git a/esphome/components/sht4x/__init__.py b/esphome/components/sht4x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py new file mode 100644 index 0000000000..bd3e5c108c --- /dev/null +++ b/esphome/components/sht4x/sensor.py @@ -0,0 +1,92 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + CONF_HUMIDITY, + UNIT_CELSIUS, + UNIT_PERCENT, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +sht4x_ns = cg.esphome_ns.namespace("sht4x") + +SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice) + +CONF_PRECISION = "precision" +SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION") +PRECISION_OPTIONS = { + "High": SHT4XPRECISION.SHT4X_PRECISION_HIGH, + "Med": SHT4XPRECISION.SHT4X_PRECISION_MED, + "Low": SHT4XPRECISION.SHT4X_PRECISION_LOW, +} + +CONF_HEATER_POWER = "heater_power" +SHT4XHEATERPOWER = sht4x_ns.enum("SHT4XHEATERPOWER") +HEATER_POWER_OPTIONS = { + "High": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_HIGH, + "Med": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_MED, + "Low": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_LOW, +} + +CONF_HEATER_TIME = "heater_time" +SHT4XHEATERTIME = sht4x_ns.enum("SHT4XHEATERTIME") +HEATER_TIME_OPTIONS = { + "Long": SHT4XHEATERTIME.SHT4X_HEATERTIME_LONG, + "Short": SHT4XHEATERTIME.SHT4X_HEATERTIME_SHORT, +} + +CONF_HEATER_MAX_DUTY = "heater_max_duty" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SHT4XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_THERMOMETER, 2, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_WATER_PERCENT, 2, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS), + cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum( + HEATER_POWER_OPTIONS + ), + cv.Optional(CONF_HEATER_TIME, default="Long"): cv.enum(HEATER_TIME_OPTIONS), + cv.Optional(CONF_HEATER_MAX_DUTY, default=0.0): cv.float_range( + min=0.0, max=0.05 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) + +TYPES = { + CONF_TEMPERATURE: "set_temp_sensor", + CONF_HUMIDITY: "set_humidity_sensor", +} + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_precision_value(config[CONF_PRECISION])) + cg.add(var.set_heater_power_value(config[CONF_HEATER_POWER])) + cg.add(var.set_heater_time_value(config[CONF_HEATER_TIME])) + cg.add(var.set_heater_duty_value(config[CONF_HEATER_MAX_DUTY])) + + for key, funcName in TYPES.items(): + + if key in config: + sens = yield sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp new file mode 100644 index 0000000000..a4b315940d --- /dev/null +++ b/esphome/components/sht4x/sht4x.cpp @@ -0,0 +1,89 @@ +#include "sht4x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sht4x { + +static const char *TAG = "sht4x"; + +static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; + +void SHT4XComponent::start_heater_() { + uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; + + ESP_LOGD(TAG, "Heater turning on"); + this->write_bytes_raw(cmd, 1); +} + +void SHT4XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sht4x..."); + + if (this->duty_cycle_ > 0.0) { + uint32_t heater_interval = (uint32_t)(this->heater_time_ / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %i", heater_interval); + + if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x39; + } else { + this->heater_command_ = 0x32; + } + } else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x2F; + } else { + this->heater_command_ = 0x24; + } + } else { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x1E; + } else { + this->heater_command_ = 0x15; + } + } + ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); + + this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); + } +} + +void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); } + +void SHT4XComponent::update() { + uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]}; + + // Send command + this->write_bytes_raw(cmd, 1); + + this->set_timeout(10, [this]() { + const uint8_t num_bytes = 6; + uint8_t buffer[num_bytes]; + + // Read measurement + bool read_status = this->read_bytes_raw(buffer, num_bytes); + + if (read_status) { + // Evaluate and publish measurements + if (this->temp_sensor_ != nullptr) { + // Temp is contained in the first 16 bits + float sensor_value_temp = (buffer[0] << 8) + buffer[1]; + float temp = -45 + 175 * sensor_value_temp / 65535; + + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + // Relative humidity is in the last 16 bits + float sensor_value_rh = (buffer[3] << 8) + buffer[4]; + float rh = -6 + 125 * sensor_value_rh / 65535; + + this->humidity_sensor_->publish_state(rh); + } + } else { + ESP_LOGD(TAG, "Sensor read failed"); + } + }); +} + +} // namespace sht4x +} // namespace esphome diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h new file mode 100644 index 0000000000..8694bd9879 --- /dev/null +++ b/esphome/components/sht4x/sht4x.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sht4x { + +enum SHT4XPRECISION { SHT4X_PRECISION_HIGH = 0, SHT4X_PRECISION_MED, SHT4X_PRECISION_LOW }; + +enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEATERPOWER_LOW }; + +enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 }; + +class SHT4XComponent : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + void set_precision_value(SHT4XPRECISION precision) { this->precision_ = precision; }; + void set_heater_power_value(SHT4XHEATERPOWER heater_power) { this->heater_power_ = heater_power; }; + void set_heater_time_value(SHT4XHEATERTIME heater_time) { this->heater_time_ = heater_time; }; + void set_heater_duty_value(float duty_cycle) { this->duty_cycle_ = duty_cycle; }; + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + SHT4XPRECISION precision_; + SHT4XHEATERPOWER heater_power_; + SHT4XHEATERTIME heater_time_; + float duty_cycle_; + + void start_heater_(); + uint8_t heater_command_; + + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace sht4x +} // namespace esphome diff --git a/esphome/components/sm2135/__init__.py b/esphome/components/sm2135/__init__.py new file mode 100644 index 0000000000..019a1c68c1 --- /dev/null +++ b/esphome/components/sm2135/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@BoukeHaarsma23"] + +sm2135_ns = cg.esphome_ns.namespace("sm2135") +SM2135 = sm2135_ns.class_("SM2135", cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM2135), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) diff --git a/esphome/components/sm2135/output.py b/esphome/components/sm2135/output.py new file mode 100644 index 0000000000..86f6db1bb4 --- /dev/null +++ b/esphome/components/sm2135/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM2135 + +DEPENDENCIES = ["sm2135"] +CODEOWNERS = ["@BoukeHaarsma23"] + +Channel = SM2135.class_("Channel", output.FloatOutput) + +CONF_SM2135_ID = "sm2135_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM2135_ID): cv.use_id(SM2135), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield output.register_output(var, config) + + parent = yield cg.get_variable(config[CONF_SM2135_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp new file mode 100644 index 0000000000..3991c220ec --- /dev/null +++ b/esphome/components/sm2135/sm2135.cpp @@ -0,0 +1,81 @@ +#include "sm2135.h" +#include "esphome/core/log.h" + +// Tnx to the work of https://github.com/arendst (Tasmota) for making the initial version of the driver + +namespace esphome { +namespace sm2135 { + +static const char *TAG = "sm2135"; + +static const uint8_t SM2135_ADDR_MC = 0xC0; // Max current register +static const uint8_t SM2135_ADDR_CH = 0xC1; // RGB or CW channel select register +static const uint8_t SM2135_ADDR_R = 0xC2; // Red color +static const uint8_t SM2135_ADDR_G = 0xC3; // Green color +static const uint8_t SM2135_ADDR_B = 0xC4; // Blue color +static const uint8_t SM2135_ADDR_C = 0xC5; // Cold +static const uint8_t SM2135_ADDR_W = 0xC6; // Warm + +static const uint8_t SM2135_RGB = 0x00; // RGB channel +static const uint8_t SM2135_CW = 0x80; // CW channel (Chip default) + +static const uint8_t SM2135_10MA = 0x00; +static const uint8_t SM2135_15MA = 0x01; +static const uint8_t SM2135_20MA = 0x02; // RGB max current (Chip default) +static const uint8_t SM2135_25MA = 0x03; +static const uint8_t SM2135_30MA = 0x04; // CW max current (Chip default) +static const uint8_t SM2135_35MA = 0x05; +static const uint8_t SM2135_40MA = 0x06; +static const uint8_t SM2135_45MA = 0x07; // Max value for RGB +static const uint8_t SM2135_50MA = 0x08; +static const uint8_t SM2135_55MA = 0x09; +static const uint8_t SM2135_60MA = 0x0A; + +static const uint8_t SM2135_CURRENT = (SM2135_20MA << 4) | SM2135_10MA; + +void SM2135::setup() { + ESP_LOGCONFIG(TAG, "Setting up SM2135OutputComponent..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->pwm_amounts_.resize(5, 0); +} +void SM2135::dump_config() { + ESP_LOGCONFIG(TAG, "SM2135:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); +} + +void SM2135::loop() { + if (!this->update_) + return; + + uint8_t data[6]; + if (this->update_channel_ == 3 || this->update_channel_ == 4) { + // No color so must be Cold/Warm + data[0] = SM2135_ADDR_MC; + data[1] = SM2135_CURRENT; + data[2] = SM2135_CW; + this->write_buffer_(data, 3); + delay(1); + data[0] = SM2135_ADDR_C; + data[1] = this->pwm_amounts_[4]; // Warm + data[2] = this->pwm_amounts_[3]; // Cold + this->write_buffer_(data, 3); + } else { + // Color + data[0] = SM2135_ADDR_MC; + data[1] = SM2135_CURRENT; + data[2] = SM2135_RGB; + data[3] = this->pwm_amounts_[1]; // Green + data[4] = this->pwm_amounts_[0]; // Red + data[5] = this->pwm_amounts_[2]; // Blue + this->write_buffer_(data, 6); + } + + this->update_ = false; +} + +} // namespace sm2135 +} // namespace esphome diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h new file mode 100644 index 0000000000..e39730579f --- /dev/null +++ b/esphome/components/sm2135/sm2135.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sm2135 { + +class SM2135 : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(SM2135 *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0xff); + this->parent_->set_channel_value_(this->channel_, amount); + } + + SM2135 *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint8_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; + } + void write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); + } + + void write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); + } + + void write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); + } + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t update_channel_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace sm2135 +} // namespace esphome diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 5241f1bb55..8cc911529b 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -1,8 +1,16 @@ +import re + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import time -from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +from esphome.const import ( + CONF_TIME_ID, + CONF_ID, + CONF_TRIGGER_ID, + CONF_LATITUDE, + CONF_LONGITUDE, +) CODEOWNERS = ["@OttoWinter"] sun_ns = cg.esphome_ns.namespace("sun") @@ -14,15 +22,13 @@ SunTrigger = sun_ns.class_( SunCondition = sun_ns.class_("SunCondition", automation.Condition) CONF_SUN_ID = "sun_id" -CONF_LATITUDE = "latitude" -CONF_LONGITUDE = "longitude" CONF_ELEVATION = "elevation" CONF_ON_SUNRISE = "on_sunrise" CONF_ON_SUNSET = "on_sunset" # Default sun elevation is a bit below horizon because sunset # means time when the entire sun disk is below the horizon -DEFAULT_ELEVATION = -0.883 +DEFAULT_ELEVATION = -0.83333 ELEVATION_MAP = { "sunrise": 0.0, @@ -45,12 +51,54 @@ def elevation(value): return cv.float_range(min=-180, max=180)(value) +# Parses sexagesimal values like 22°57′7″S +LAT_LON_REGEX = re.compile( + r"([+\-])?\s*" + r"(?:([0-9]+)\s*°)?\s*" + r"(?:([0-9]+)\s*[′\'])?\s*" + r'(?:([0-9]+)\s*[″"])?\s*' + r"([NESW])?" +) + + +def parse_latlon(value): + if isinstance(value, str) and value.endswith("°"): + # strip trailing degree character + value = value[:-1] + try: + return cv.float_(value) + except cv.Invalid: + pass + + value = cv.string_strict(value) + m = LAT_LON_REGEX.match(value) + + if m is None: + raise cv.Invalid("Invalid format for latitude/longitude") + sign = m.group(1) + deg = m.group(2) + minute = m.group(3) + second = m.group(4) + d = m.group(5) + + val = float(deg or 0) + float(minute or 0) / 60 + float(second or 0) / 3600 + if sign == "-": + val *= -1 + if d and d in "SW": + val *= -1 + return val + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Sun), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_LATITUDE): cv.float_range(min=-90, max=90), - cv.Required(CONF_LONGITUDE): cv.float_range(min=-180, max=180), + cv.Required(CONF_LATITUDE): cv.All( + parse_latlon, cv.float_range(min=-90, max=90) + ), + cv.Required(CONF_LONGITUDE): cv.All( + parse_latlon, cv.float_range(min=-180, max=180) + ), cv.Optional(CONF_ON_SUNRISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 6744a418c5..0722e66e67 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -1,176 +1,319 @@ #include "sun.h" #include "esphome/core/log.h" +/* +The formulas/algorithms in this module are based on the book +"Astronomical algorithms" by Jean Meeus (2nd edition) + +The target accuracy of this implementation is ~1min for sunrise/sunset calculations, +and 6 arcminutes for elevation/azimuth. As such, some of the advanced correction factors +like exact nutation are not included. But in some testing the accuracy appears to be within range +for random spots around the globe. +*/ + namespace esphome { namespace sun { +using namespace esphome::sun::internal; + static const char *TAG = "sun"; #undef PI +#undef degrees +#undef radians +#undef sq -/* Usually, ESPHome uses single-precision floating point values - * because those tend to be accurate enough and are more efficient. - * - * However, some of the data in this class has to be quite accurate, so double is - * used everywhere. - */ -static const double PI = 3.141592653589793; -static const double TAU = 6.283185307179586; -static const double TO_RADIANS = PI / 180.0; -static const double TO_DEGREES = 180.0 / PI; -static const double EARTH_TILT = 23.44 * TO_RADIANS; +static const num_t PI = 3.141592653589793; +inline num_t degrees(num_t rad) { return rad * 180 / PI; } +inline num_t radians(num_t deg) { return deg * PI / 180; } +inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } +inline num_t sq(num_t x) { return x * x; } +inline num_t cb(num_t x) { return x * x * x; } -optional Sun::sunrise(double elevation) { - auto time = this->time_->now(); - if (!time.is_valid()) - return {}; - double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true); - if (isnan(sun_time)) - return {}; - uint32_t epoch = this->calc_epoch_(time, sun_time); - return time::ESPTime::from_epoch_local(epoch); -} -optional Sun::sunset(double elevation) { - auto time = this->time_->now(); - if (!time.is_valid()) - return {}; - double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false); - if (isnan(sun_time)) - return {}; - uint32_t epoch = this->calc_epoch_(time, sun_time); - return time::ESPTime::from_epoch_local(epoch); -} -double Sun::elevation() { - auto time = this->current_sun_time_(); - if (isnan(time)) - return NAN; - return this->elevation_(time); -} -double Sun::azimuth() { - auto time = this->current_sun_time_(); - if (isnan(time)) - return NAN; - return this->azimuth_(time); -} -// like clamp, but with doubles -double clampd(double val, double min, double max) { - if (val < min) - return min; - if (val > max) - return max; - return val; -} -double Sun::sun_declination_(double sun_time) { - double n = sun_time - 1.0; - // maximum declination - const double tot = -sin(EARTH_TILT); +num_t GeoLocation::latitude_rad() const { return radians(latitude); } +num_t GeoLocation::longitude_rad() const { return radians(longitude); } +num_t EquatorialCoordinate::right_ascension_rad() const { return radians(right_ascension); } +num_t EquatorialCoordinate::declination_rad() const { return radians(declination); } +num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); } +num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); } - // eccentricity of the earth's orbit (ellipse) - double eccentricity = 0.0167; - - // days since perihelion (January 3rd) - double days_since_perihelion = n - 2; - // days since december solstice (december 22) - double days_since_december_solstice = n + 10; - const double c = TAU / 365.24; - double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion)); - // Make sure value is in range (double error may lead to results slightly larger than 1) - double x = clampd(tot * v, -1.0, 1.0); - return asin(x); +num_t julian_day(time::ESPTime moment) { + // p. 59 + // UT -> JD, TT -> JDE + int y = moment.year; + int m = moment.month; + num_t d = moment.day_of_month; + d += moment.hour / 24.0; + d += moment.minute / (24.0 * 60); + d += moment.second / (24.0 * 60 * 60); + if (m <= 2) { + y -= 1; + m += 12; + } + int a = y / 100; + int b = 2 - a + a / 4; + return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5; } -double Sun::elevation_ratio_(double sun_time) { - double decl = this->sun_declination_(sun_time); - double hangle = this->hour_angle_(sun_time); - double a = sin(this->latitude_rad_()) * sin(decl); - double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle); - double val = clampd(a + b, -1.0, 1.0); - return val; +num_t delta_t(time::ESPTime moment) { + // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html) + int t = moment.year - 2000; + return 62.92 + t * (0.32217 + t * 0.005589); } -double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; } -double Sun::hour_angle_(double sun_time) { - double time_of_day = fmod(sun_time, 1.0) * 24.0; - return -PI * (time_of_day - 12) / 12; +// Perform a fractional module operation where the result will always be positive (wrapping around) +num_t wmod(num_t x, num_t y) { + num_t res = fmod(x, y); + if (res < 0) + res += y; + return res; } -double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; } -double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); } -double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); } -double Sun::azimuth_rad_(double sun_time) { - double hangle = -this->hour_angle_(sun_time); - double decl = this->sun_declination_(sun_time); - double zen = this->zenith_rad_(sun_time); - double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl); - double denom = sin(zen) * cos(this->latitude_rad_()); - double v = clampd(nom / denom, -1.0, 1.0); - double az = PI - acos(v); - if (hangle > 0) - az = -az; - if (az < 0) - az += TAU; - return az; + +num_t internal::Moment::jd() const { return julian_day(dt); } + +num_t internal::Moment::jde() const { + // dt is in UT1, but JDE is based on TT + // so add deltaT factor + return jd() + delta_t(dt) / (60 * 60 * 24); } -double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; } -double Sun::calc_sun_time_(const time::ESPTime &time) { - // Time as seen at 0° longitude - if (!time.is_valid()) - return NAN; - double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0); - // Add longitude correction - double add = this->longitude_ / 360.0; - return base + add; -} -uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) { - sun_time -= this->longitude_ / 360.0; - base.day_of_year = uint32_t(floor(sun_time)); +struct SunAtTime { + num_t jde; + num_t t; - sun_time = (sun_time - base.day_of_year) * 24.0; - base.hour = uint32_t(floor(sun_time)); - - sun_time = (sun_time - base.hour) * 60.0; - base.minute = uint32_t(floor(sun_time)); - - sun_time = (sun_time - base.minute) * 60.0; - base.second = uint32_t(floor(sun_time)); - - base.recalc_timestamp_utc(true); - return base.timestamp; -} -double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) { - // Use binary search, newton's method would be better but binary search already - // converges quite well (19 cycles) and much simpler. Function is guaranteed to be - // monotonous. - double lo, hi; - if (rising) { - lo = day_of_year + 0.0; - hi = day_of_year + 0.5; - } else { - lo = day_of_year + 1.0; - hi = day_of_year + 0.5; + SunAtTime(num_t jde) : jde(jde) { + // eq 25.1, p. 163; julian centuries from the epoch J2000.0 + t = (jde - 2451545) / 36525.0; } - double min_elevation = this->elevation_(lo); - double max_elevation = this->elevation_(hi); - if (elevation < min_elevation || elevation > max_elevation) - return NAN; - - // Accuracy: 0.1s - const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0); - - while (fabs(hi - lo) > accuracy) { - double mid = (lo + hi) / 2.0; - double value = this->elevation_(mid) - elevation; - if (value < 0) { - lo = mid; - } else if (value > 0) { - hi = mid; - } else { - lo = hi = mid; - break; - } + num_t mean_obliquity() const { + // eq. 22.2, p. 147; mean obliquity of the ecliptic + num_t epsilon_0 = (+arcdeg(23, 26, 21.448) - arcdeg(0, 0, 46.8150) * t - arcdeg(0, 0, 0.00059) * sq(t) + + arcdeg(0, 0, 0.001813) * cb(t)); + return epsilon_0; } - return (lo + hi) / 2.0; + num_t omega() const { + // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic + // in degrees + num_t omega = 125.05 - 1934.136 * t; + return omega; + } + + num_t true_obliquity() const { + // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic + num_t delta_epsilon = 0.00256 * cos(radians(omega())); + num_t epsilon = mean_obliquity() + delta_epsilon; + return epsilon; + } + + num_t mean_longitude() const { + // eq 25.2, p. 163; geometric mean longitude = mean equinox of the date in degrees + num_t l0 = 280.46646 + 36000.76983 * t + 0.0003032 * sq(t); + return wmod(l0, 360); + } + + num_t eccentricity() const { + // eq 25.4, p. 163; eccentricity of earth's orbit + num_t e = 0.016708634 - 0.000042037 * t - 0.0000001267 * sq(t); + return e; + } + + num_t mean_anomaly() const { + // eq 25.3, p. 163; mean anomaly of the sun in degrees + num_t m = 357.52911 + 35999.05029 * t - 0.0001537 * sq(t); + return wmod(m, 360); + } + + num_t equation_of_center() const { + // p. 164; sun's equation of the center c in degrees + num_t m_rad = radians(mean_anomaly()); + num_t c = ((1.914602 - 0.004817 * t - 0.000014 * sq(t)) * sin(m_rad) + (0.019993 - 0.000101 * t) * sin(2 * m_rad) + + 0.000289 * sin(3 * m_rad)); + return wmod(c, 360); + } + + num_t true_longitude() const { + // p. 164; sun's true longitude in degrees + num_t x = mean_longitude() + equation_of_center(); + return wmod(x, 360); + } + + num_t true_anomaly() const { + // p. 164; sun's true anomaly in degrees + num_t x = mean_anomaly() + equation_of_center(); + return wmod(x, 360); + } + + num_t apparent_longitude() const { + // p. 164; sun's apparent longitude = true equinox in degrees + num_t x = true_longitude() - 0.00569 - 0.00478 * sin(radians(omega())); + return wmod(x, 360); + } + + EquatorialCoordinate equatorial_coordinate() const { + num_t epsilon_rad = radians(true_obliquity()); + // eq. 25.6; p. 165; sun's right ascension alpha + num_t app_lon_rad = radians(apparent_longitude()); + num_t right_ascension_rad = atan2(cos(epsilon_rad) * sin(app_lon_rad), cos(app_lon_rad)); + num_t declination_rad = asin(sin(epsilon_rad) * sin(app_lon_rad)); + return EquatorialCoordinate{degrees(right_ascension_rad), degrees(declination_rad)}; + } + + num_t equation_of_time() const { + // chapter 28, p. 185 + num_t epsilon_half = radians(true_obliquity() / 2); + num_t y = sq(tan(epsilon_half)); + num_t l2 = 2 * mean_longitude(); + num_t l2_rad = radians(l2); + num_t e = eccentricity(); + num_t m = mean_anomaly(); + num_t m_rad = radians(m); + num_t sin_m = sin(m_rad); + num_t eot = (y * sin(l2_rad) - 2 * e * sin_m + 4 * e * y * sin_m * cos(l2_rad) - 1 / 2.0 * sq(y) * sin(2 * l2_rad) - + 5 / 4.0 * sq(e) * sin(2 * m_rad)); + return degrees(eot); + } + + void debug() const { + // debug output like in example 25.a, p. 165 + ESP_LOGV(TAG, "jde: %f", jde); + ESP_LOGV(TAG, "T: %f", t); + ESP_LOGV(TAG, "L_0: %f", mean_longitude()); + ESP_LOGV(TAG, "M: %f", mean_anomaly()); + ESP_LOGV(TAG, "e: %f", eccentricity()); + ESP_LOGV(TAG, "C: %f", equation_of_center()); + ESP_LOGV(TAG, "Odot: %f", true_longitude()); + ESP_LOGV(TAG, "Omega: %f", omega()); + ESP_LOGV(TAG, "lambda: %f", apparent_longitude()); + ESP_LOGV(TAG, "epsilon_0: %f", mean_obliquity()); + ESP_LOGV(TAG, "epsilon: %f", true_obliquity()); + ESP_LOGV(TAG, "v: %f", true_anomaly()); + auto eq = equatorial_coordinate(); + ESP_LOGV(TAG, "right_ascension: %f", eq.right_ascension); + ESP_LOGV(TAG, "declination: %f", eq.declination); + } +}; + +struct SunAtLocation { + GeoLocation location; + + num_t greenwich_sidereal_time(Moment moment) const { + // Return the greenwich mean sidereal time for this instant in degrees + // see chapter 12, p. 87 + num_t jd = moment.jd(); + // eq 12.1, p.87; jd for 0h UT of this date + time::ESPTime moment_0h = moment.dt; + moment_0h.hour = moment_0h.minute = moment_0h.second = 0; + num_t jd0 = Moment{moment_0h}.jd(); + num_t t = (jd0 - 2451545) / 36525; + // eq. 12.4, p.88 + num_t gmst = (+280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * sq(t) - (1 / 38710000.0) * cb(t)); + return wmod(gmst, 360); + } + + HorizontalCoordinate true_coordinate(Moment moment) const { + auto eq = SunAtTime(moment.jde()).equatorial_coordinate(); + num_t gmst = greenwich_sidereal_time(moment); + // do not apply any nutation correction (not important for our target accuracy) + num_t nutation_corr = 0; + + num_t ra = eq.right_ascension; + num_t alpha = gmst + nutation_corr + location.longitude - ra; + alpha = wmod(alpha, 360); + num_t alpha_rad = radians(alpha); + + num_t sin_lat = sin(location.latitude_rad()); + num_t cos_lat = cos(location.latitude_rad()); + num_t sin_elevation = (+sin_lat * sin(eq.declination_rad()) + cos_lat * cos(eq.declination_rad()) * cos(alpha_rad)); + num_t elevation_rad = asin(sin_elevation); + num_t azimuth_rad = atan2(sin(alpha_rad), cos(alpha_rad) * sin_lat - tan(eq.declination_rad()) * cos_lat); + return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180}; + } + + optional sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); } + optional sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); } + optional event(bool rise, time::ESPTime date, num_t zenith) const { + // couldn't get the method described in chapter 15 to work, + // so instead this is based on the algorithm in time4j + // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java + auto m = local_event_(date, 12); // noon + num_t jde = julian_day(m); + num_t new_h = 0, old_h; + do { + old_h = new_h; + auto x = local_hour_angle_(jde + old_h / 86400, rise, zenith); + if (!x.has_value()) + return {}; + new_h = *x; + } while (std::abs(new_h - old_h) >= 15); + time_t new_timestamp = m.timestamp + (time_t) new_h; + return time::ESPTime::from_epoch_local(new_timestamp); + } + + protected: + optional local_hour_angle_(num_t jde, bool rise, num_t zenith) const { + auto pos = SunAtTime(jde).equatorial_coordinate(); + num_t dec_rad = pos.declination_rad(); + num_t lat_rad = location.latitude_rad(); + num_t num = cos(radians(zenith)) - (sin(dec_rad) * sin(lat_rad)); + num_t denom = cos(dec_rad) * cos(lat_rad); + num_t cos_h = num / denom; + if (cos_h > 1 || cos_h < -1) + return {}; + num_t hour_angle = degrees(acos(cos_h)) * 240; + if (rise) + hour_angle *= -1; + return hour_angle; + } + + time::ESPTime local_event_(time::ESPTime date, int hour) const { + // input date should be in UTC, and hour/minute/second fields 0 + num_t added_d = hour / 24.0 - location.longitude / 360; + num_t jd = julian_day(date) + added_d; + + num_t eot = SunAtTime(jd).equation_of_time() * 240; + time_t new_timestamp = (time_t)(date.timestamp + added_d * 86400 - eot); + return time::ESPTime::from_epoch_utc(new_timestamp); + } +}; + +HorizontalCoordinate Sun::calc_coords_() { + SunAtLocation sun{location_}; + Moment m{time_->utcnow()}; + if (!m.dt.is_valid()) + return HorizontalCoordinate{NAN, NAN}; + + // uncomment to print some debug output + /* + SunAtTime st{m.jde()}; + st.debug(); + */ + return sun.true_coordinate(m); } +optional Sun::calc_event_(bool rising, double zenith) { + SunAtLocation sun{location_}; + auto now = this->time_->utcnow(); + if (!now.is_valid()) + return {}; + // Calculate UT1 timestamp at 0h + auto today = now; + today.hour = today.minute = today.second = 0; + today.recalc_timestamp_utc(); + + auto it = sun.event(rising, today, zenith); + if (it.has_value() && it->timestamp < now.timestamp) { + // We're calculating *next* sunrise/sunset, but calculated event + // is today, so try again tomorrow + time_t new_timestamp = today.timestamp + 24 * 60 * 60; + today = time::ESPTime::from_epoch_utc(new_timestamp); + it = sun.event(rising, today, zenith); + } + return it; +} + +optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } +optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +double Sun::elevation() { return this->calc_coords_().elevation; } +double Sun::azimuth() { return this->calc_coords_().azimuth; } } // namespace sun } // namespace esphome diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 501d122da0..6a8364a5f0 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -8,85 +8,72 @@ namespace esphome { namespace sun { +namespace internal { + +/* Usually, ESPHome uses single-precision floating point values + * because those tend to be accurate enough and are more efficient. + * + * However, some of the data in this class has to be quite accurate, so double is + * used everywhere. + */ +using num_t = double; +struct GeoLocation { + num_t latitude; + num_t longitude; + + num_t latitude_rad() const; + num_t longitude_rad() const; +}; + +struct Moment { + time::ESPTime dt; + + num_t jd() const; + num_t jde() const; +}; + +struct EquatorialCoordinate { + num_t right_ascension; + num_t declination; + + num_t right_ascension_rad() const; + num_t declination_rad() const; +}; + +struct HorizontalCoordinate { + num_t elevation; + num_t azimuth; + + num_t elevation_rad() const; + num_t azimuth_rad() const; +}; + +} // namespace internal + class Sun { public: void set_time(time::RealTimeClock *time) { time_ = time; } time::RealTimeClock *get_time() const { return time_; } - void set_latitude(double latitude) { latitude_ = latitude; } - void set_longitude(double longitude) { longitude_ = longitude; } + void set_latitude(double latitude) { location_.latitude = latitude; } + void set_longitude(double longitude) { location_.longitude = longitude; } - optional sunrise(double elevation = 0.0); - optional sunset(double elevation = 0.0); + optional sunrise(double elevation); + optional sunset(double elevation); double elevation(); double azimuth(); protected: - double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); } - - /** Calculate the declination of the sun in rad. - * - * See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth - * - * Accuracy: ±0.2° - * - * @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_. - * @return Sun declination in degrees - */ - double sun_declination_(double sun_time); - - double elevation_ratio_(double sun_time); - - /** Calculate the hour angle based on the sun time of day in hours. - * - * Positive in morning, 0 at noon, negative in afternoon. - * - * @param sun_time Sun time, see calc_sun_time_. - * @return Hour angle in rad. - */ - double hour_angle_(double sun_time); - - double elevation_(double sun_time); - - double elevation_rad_(double sun_time); - - double zenith_rad_(double sun_time); - - double azimuth_rad_(double sun_time); - - double azimuth_(double sun_time); - - /** Return the sun time given by the time_ object. - * - * Sun time is defined as doubleing point day of year. - * Integer part encodes the day of the year (1=January 1st) - * Decimal part encodes time of day (1/24 = 1 hour) - */ - double calc_sun_time_(const time::ESPTime &time); - - uint32_t calc_epoch_(time::ESPTime base, double sun_time); - - /** Calculate the sun time of day - * - * @param day_of_year - * @param elevation - * @param rising - * @return - */ - double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising); - - double latitude_rad_(); + internal::HorizontalCoordinate calc_coords_(); + optional calc_event_(bool rising, double zenith); time::RealTimeClock *time_; - /// Latitude in degrees, range: -90 to 90. - double latitude_; - /// Longitude in degrees, range: -180 to 180. - double longitude_; + internal::GeoLocation location_; }; class SunTrigger : public Trigger<>, public PollingComponent, public Parented { public: - SunTrigger() : PollingComponent(1000) {} + SunTrigger() : PollingComponent(60000) {} void set_sunrise(bool sunrise) { sunrise_ = sunrise; } void set_elevation(double elevation) { elevation_ = elevation; } diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py new file mode 100644 index 0000000000..aedd751086 --- /dev/null +++ b/esphome/components/tca9548a/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_SCAN + +CODEOWNERS = ["@andreashergert1984"] + +DEPENDENCIES = ["i2c"] + +tca9548a_ns = cg.esphome_ns.namespace("tca9548a") +TCA9548AComponent = tca9548a_ns.class_( + "TCA9548AComponent", cg.PollingComponent, i2c.I2CMultiplexer +) + +MULTI_CONF = True + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TCA9548AComponent), + cv.Optional(CONF_SCAN, default=True): cv.boolean, + } +).extend(i2c.i2c_device_schema(0x70)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add_define("USE_I2C_MULTIPLEXER") + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + cg.add(var.set_scan(config[CONF_SCAN])) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp new file mode 100644 index 0000000000..0df60d6dd2 --- /dev/null +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -0,0 +1,41 @@ +#include "tca9548a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tca9548a { + +static const char *TAG = "tca9548a"; + +void TCA9548AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); + uint8_t status = 0; + if (!this->read_byte(0x00, &status)) { + ESP_LOGI(TAG, "TCA9548A failed"); + return; + } + // out of range to make sure on first set_channel a new one will be set + this->current_channelno_ = 8; + ESP_LOGCONFIG(TAG, "Channels currently open: %d", status); +} +void TCA9548AComponent::dump_config() { + ESP_LOGCONFIG(TAG, "TCA9548A:"); + LOG_I2C_DEVICE(this); + if (this->scan_) { + for (uint8_t i = 0; i < 8; i++) { + ESP_LOGCONFIG(TAG, "Activating channel: %d", i); + this->set_channel(i); + this->parent_->dump_config(); + } + } +} + +void TCA9548AComponent::set_channel(uint8_t channelno) { + if (this->current_channelno_ != channelno) { + this->current_channelno_ = channelno; + uint8_t channelbyte = 1 << channelno; + this->write_byte(0x70, channelbyte); + } +} + +} // namespace tca9548a +} // namespace esphome diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h new file mode 100644 index 0000000000..50b1eb8b56 --- /dev/null +++ b/esphome/components/tca9548a/tca9548a.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tca9548a { + +class TCA9548AComponent : public Component, public i2c::I2CMultiplexer { + public: + void set_scan(bool scan) { scan_ = scan; } + void setup() override; + void dump_config() override; + void update(); + void set_channel(uint8_t channelno) override; + + protected: + bool scan_; + uint8_t current_channelno_; +}; +} // namespace tca9548a +} // namespace esphome diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index b1c938c18e..8fbc2dcaf6 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -138,7 +138,7 @@ def convert_tz(pytz_obj): _tz_dst_str(dst_ends_local), ) _LOGGER.info( - "Detected timezone '%s' with UTC offset %s and daylight savings time from " + "Detected timezone '%s' with UTC offset %s and daylight saving time from " "%s to %s", tzname_off, _tz_timedelta(utcoffset_off), diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index a809401c33..c591482729 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -30,7 +30,7 @@ struct ESPTime { uint8_t month; /// year uint16_t year; - /// daylight savings time flag + /// daylight saving time flag bool is_dst; union { ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time; diff --git a/esphome/components/tof10120/__init__.py b/esphome/components/tof10120/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py new file mode 100644 index 0000000000..91a15960b4 --- /dev/null +++ b/esphome/components/tof10120/sensor.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL + +CODEOWNERS = ["@wstrzalka"] +DEPENDENCIES = ["i2c"] + +tof10120_ns = cg.esphome_ns.namespace("tof10120") +TOF10120Sensor = tof10120_ns.class_( + "TOF10120Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 3) + .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)}) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x52)) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp new file mode 100644 index 0000000000..4e2732bb08 --- /dev/null +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -0,0 +1,53 @@ +#include "tof10120_sensor.h" +#include "esphome/core/log.h" + +// Very basic support for TOF10120 distance sensor + +namespace esphome { +namespace tof10120 { + +static const char *TAG = "tof10120"; +static const uint8_t TOF10120_READ_DISTANCE_CMD[] = {0x00}; +static const uint8_t TOF10120_DEFAULT_DELAY = 30; + +static const uint8_t TOF10120_DIR_SEND_REGISTER = 0x0e; +static const uint8_t TOF10120_DISTANCE_REGISTER = 0x00; + +static const uint16_t TOF10120_OUT_OF_RANGE_VALUE = 2000; + +void TOF10120Sensor::dump_config() { + LOG_SENSOR("", "TOF10120", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} + +void TOF10120Sensor::setup() {} + +void TOF10120Sensor::update() { + if (!this->write_bytes(TOF10120_DISTANCE_REGISTER, TOF10120_READ_DISTANCE_CMD, sizeof(TOF10120_READ_DISTANCE_CMD))) { + ESP_LOGE(TAG, "Communication with TOF10120 failed on write"); + this->status_set_warning(); + return; + } + + uint8_t data[2]; + if (!this->read_bytes(TOF10120_DISTANCE_REGISTER, data, 2, TOF10120_DEFAULT_DELAY)) { + ESP_LOGE(TAG, "Communication with TOF10120 failed on read"); + this->status_set_warning(); + return; + } + + uint32_t distance_mm = (data[0] << 8) | data[1]; + ESP_LOGI(TAG, "Data read: %dmm", distance_mm); + + if (distance_mm == TOF10120_OUT_OF_RANGE_VALUE) { + ESP_LOGW(TAG, "Distance measurement out of range"); + this->publish_state(NAN); + } else { + this->publish_state(distance_mm / 1000.0); + } + this->status_clear_warning(); +} + +} // namespace tof10120 +} // namespace esphome diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h new file mode 100644 index 0000000000..90bad8ed07 --- /dev/null +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tof10120 { + +class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; +}; +} // namespace tof10120 +} // namespace esphome diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index 8615f3ae85..5d3345a5c4 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,13 +1,14 @@ from esphome.components import fan import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT +from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] CONF_SPEED_DATAPOINT = "speed_datapoint" CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" +CONF_DIRECTION_DATAPOINT = "direction_datapoint" TuyaFan = tuya_ns.class_("TuyaFan", cg.Component) @@ -19,6 +20,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), @@ -26,13 +29,13 @@ CONFIG_SCHEMA = cv.All( def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) + parent = yield cg.get_variable(config[CONF_TUYA_ID]) + state = yield fan.create_fan_state(config) - paren = yield cg.get_variable(config[CONF_TUYA_ID]) - fan_ = yield fan.create_fan_state(config) - cg.add(var.set_tuya_parent(paren)) - cg.add(var.set_fan(fan_)) + var = cg.new_Pvariable( + config[CONF_OUTPUT_ID], parent, state, config[CONF_SPEED_COUNT] + ) + yield cg.register_component(var, config) if CONF_SPEED_DATAPOINT in config: cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT])) @@ -40,3 +43,5 @@ def to_code(config): cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) if CONF_OSCILLATION_DATAPOINT in config: cg.add(var.set_oscillation_id(config[CONF_OSCILLATION_DATAPOINT])) + if CONF_DIRECTION_DATAPOINT in config: + cg.add(var.set_direction_id(config[CONF_DIRECTION_DATAPOINT])) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 718f292f5b..62f4a78db7 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -8,18 +8,15 @@ namespace tuya { static const char *TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false, 3); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), + this->direction_id_.has_value(), this->speed_count_); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) { auto call = this->fan_->make_call(); - if (datapoint.value_enum == 0x0) - call.set_speed(1); - else if (datapoint.value_enum == 0x1) - call.set_speed(2); - else if (datapoint.value_enum == 0x2) - call.set_speed(3); + if (datapoint.value_enum < this->speed_count_) + call.set_speed(datapoint.value_enum + 1); else ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum); ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum); @@ -42,17 +39,29 @@ void TuyaFan::setup() { ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); }); } + if (this->direction_id_.has_value()) { + this->parent_->register_listener(*this->direction_id_, [this](TuyaDatapoint datapoint) { + auto call = this->fan_->make_call(); + call.set_direction(datapoint.value_bool ? fan::FAN_DIRECTION_REVERSE : fan::FAN_DIRECTION_FORWARD); + call.perform(); + ESP_LOGD(TAG, "MCU reported reverse direction is: %s", ONOFF(datapoint.value_bool)); + }); + } + this->fan_->add_on_state_callback([this]() { this->write_state(); }); } void TuyaFan::dump_config() { ESP_LOGCONFIG(TAG, "Tuya Fan:"); + ESP_LOGCONFIG(TAG, " Speed count %d", this->speed_count_); if (this->speed_id_.has_value()) ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); if (this->switch_id_.has_value()) ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); if (this->oscillation_id_.has_value()) ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); + if (this->direction_id_.has_value()) + ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); } void TuyaFan::write_state() { @@ -72,6 +81,15 @@ void TuyaFan::write_state() { this->parent_->set_datapoint_value(datapoint); ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating)); } + if (this->direction_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->direction_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + datapoint.value_bool = enable; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } if (this->speed_id_.has_value()) { TuyaDatapoint datapoint{}; datapoint.id = *this->speed_id_; diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index d31d490e1a..a24e7a218e 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -9,25 +9,28 @@ namespace tuya { class TuyaFan : public Component { public: + TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {} void setup() override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } - void set_fan(fan::FanState *fan) { this->fan_ = fan; } - void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; } void write_state(); protected: void update_speed_(uint32_t value); void update_switch_(uint32_t value); void update_oscillation_(uint32_t value); + void update_direction_(uint32_t value); Tuya *parent_; optional speed_id_{}; optional switch_id_{}; optional oscillation_id_{}; + optional direction_id_{}; fan::FanState *fan_; + int speed_count_{}; }; } // namespace tuya diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp index 38eadc9dc8..9bf34490ec 100644 --- a/esphome/components/uln2003/uln2003.cpp +++ b/esphome/components/uln2003/uln2003.cpp @@ -14,8 +14,8 @@ void ULN2003::setup() { this->loop(); } void ULN2003::loop() { - bool at_target = this->has_reached_target(); - if (at_target) { + int dir = this->should_step_(); + if (dir == 0 && this->has_reached_target()) { this->high_freq_.stop(); if (this->sleep_when_done_) { @@ -28,8 +28,6 @@ void ULN2003::loop() { } } else { this->high_freq_.start(); - - int dir = this->should_step_(); this->current_uln_pos_ += dir; } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index f5b7340ad6..421797eb28 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +from esphome.components.network import add_mdns_library from esphome.const import ( CONF_AP, CONF_BSSID, @@ -22,6 +23,7 @@ from esphome.const import ( CONF_STATIC_IP, CONF_SUBNET, CONF_USE_ADDRESS, + CONF_ENABLE_MDNS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, @@ -187,6 +189,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -298,6 +301,9 @@ def to_code(config): cg.add_define("USE_WIFI") + if config[CONF_ENABLE_MDNS]: + add_mdns_library() + # Register at end for OTA safe mode yield cg.register_component(var, config) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index df80c5b109..0a6607852d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -62,7 +62,7 @@ void WiFiComponent::setup() { } this->wifi_apply_hostname_(); -#ifdef ARDUINO_ARCH_ESP32 +#if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS) network_setup_mdns(); #endif } @@ -171,7 +171,7 @@ void WiFiComponent::setup_ap_config_() { this->ap_setup_ = this->wifi_start_ap_(this->ap_); ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().toString().c_str()); -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) network_setup_mdns(this->wifi_soft_ap_ip(), 1); #endif @@ -466,7 +466,7 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGD(TAG, "Disabling AP..."); this->wifi_mode_({}, false); } -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) network_setup_mdns(this->wifi_sta_ip_(), 0); #endif this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 536d914a36..ee8fd208b2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -261,7 +261,11 @@ class WiFiComponent : public Component { #endif #ifdef ARDUINO_ARCH_ESP32 +#if ESP_IDF_VERSION_MAJOR >= 4 + void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); +#else void wifi_event_callback_(system_event_id_t event, system_event_info_t info); +#endif void wifi_scan_done_callback_(); #endif diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 09b8433a0e..cf65ed3725 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -265,7 +265,14 @@ const char *get_auth_mode_str(uint8_t mode) { return "UNKNOWN"; } } -std::string format_ip4_addr(const ip4_addr_t &ip) { + +#if ESP_IDF_VERSION_MAJOR >= 4 +using esphome_ip4_addr_t = esp_ip4_addr_t; +#else +using esphome_ip4_addr_t = ip4_addr_t; +#endif + +std::string format_ip4_addr(const esphome_ip4_addr_t &ip) { char buf[20]; sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), uint8_t(ip.addr >> 24)); @@ -346,14 +353,22 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Unspecified"; } } +#if ESP_IDF_VERSION_MAJOR >= 4 +void WiFiComponent::wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info) { +#else void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_info_t info) { +#endif switch (event) { case SYSTEM_EVENT_WIFI_READY: { ESP_LOGV(TAG, "Event: WiFi ready"); break; } case SYSTEM_EVENT_SCAN_DONE: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_scan_done; +#else auto it = info.scan_done; +#endif ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); break; } @@ -366,7 +381,11 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i break; } case SYSTEM_EVENT_STA_CONNECTED: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_sta_connected; +#else auto it = info.connected; +#endif char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; @@ -375,7 +394,11 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i break; } case SYSTEM_EVENT_STA_DISCONNECTED: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_sta_disconnected; +#else auto it = info.disconnected; +#endif char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; @@ -388,7 +411,11 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i break; } case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_sta_authmode_change; +#else auto it = info.auth_change; +#endif ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 @@ -424,13 +451,25 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i break; } case SYSTEM_EVENT_AP_STACONNECTED: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_sta_connected; + auto &mac = it.bssid; +#else auto it = info.sta_connected; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + auto &mac = it.mac; +#endif + ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } case SYSTEM_EVENT_AP_STADISCONNECTED: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_sta_disconnected; + auto &mac = it.bssid; +#else auto it = info.sta_disconnected; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + auto &mac = it.mac; +#endif + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } case SYSTEM_EVENT_AP_STAIPASSIGNED: { @@ -438,7 +477,11 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i break; } case SYSTEM_EVENT_AP_PROBEREQRECVED: { +#if ESP_IDF_VERSION_MAJOR >= 4 + auto it = info.wifi_ap_probereqrecved; +#else auto it = info.ap_probereqrecved; +#endif ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } @@ -446,8 +489,13 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i break; } +#if ESP_IDF_VERSION_MAJOR >= 4 + if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + uint8_t reason = info.wifi_sta_disconnected.reason; +#else if (event == SYSTEM_EVENT_STA_DISCONNECTED) { uint8_t reason = info.disconnected.reason; +#endif if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT || reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL || reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { @@ -458,7 +506,11 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i this->error_from_callback_ = true; } } +#if ESP_IDF_VERSION_MAJOR >= 4 + if (event == ARDUINO_EVENT_WIFI_SCAN_DONE) { +#else if (event == SYSTEM_EVENT_SCAN_DONE) { +#endif this->wifi_scan_done_callback_(); } } diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index f2a9f5408c..891d95d33e 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -5,7 +5,7 @@ from esphome.const import ( CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, ICON_EMPTY, - UNIT_DECIBEL, + UNIT_DECIBEL_MILLIWATT, ) DEPENDENCIES = ["wifi"] @@ -15,7 +15,9 @@ WiFiSignalSensor = wifi_signal_ns.class_( ) CONFIG_SCHEMA = ( - sensor.sensor_schema(UNIT_DECIBEL, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH) + sensor.sensor_schema( + UNIT_DECIBEL_MILLIWATT, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH + ) .extend( { cv.GenerateID(): cv.declare_id(WiFiSignalSensor), diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 3c26beeed4..f3d8fbd082 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -92,8 +92,14 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p switch (protocol) { case WLED_NOTIFIER: - if (!parse_notifier_frame_(it, payload, size)) - return false; + // Hyperion Port + if (port_ == 19446) { + if (!parse_drgb_frame_(it, payload, size)) + return false; + } else { + if (!parse_notifier_frame_(it, payload, size)) + return false; + } break; case WARLS: diff --git a/esphome/config.py b/esphome/config.py index 0c8e51fdce..a1fc07a21f 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,200 +1,39 @@ import collections -import importlib import logging import re -import os.path # pylint: disable=unused-import, wrong-import-order -import sys from contextlib import contextmanager import voluptuous as vol -from esphome import core, core_config, yaml_util +from esphome import core, yaml_util, loader +import esphome.core.config as core_config from esphome.const import ( CONF_ESPHOME, CONF_PLATFORM, - ESP_PLATFORMS, CONF_PACKAGES, CONF_SUBSTITUTIONS, + CONF_EXTERNAL_COMPONENTS, ) -from esphome.core import CORE, EsphomeError # noqa +from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict -from typing import List, Optional, Tuple, Union # noqa -from esphome.core import ConfigType # noqa +from typing import List, Optional, Tuple, Union +from esphome.core import ConfigType +from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid from esphome.log import color, Fore _LOGGER = logging.getLogger(__name__) -_COMPONENT_CACHE = {} - - -class ComponentManifest: - def __init__(self, module, base_components_path, is_core=False, is_platform=False): - self.module = module - self._is_core = is_core - self.is_platform = is_platform - self.base_components_path = base_components_path - - @property - def is_platform_component(self): - return getattr(self.module, "IS_PLATFORM_COMPONENT", False) - - @property - def config_schema(self): - return getattr(self.module, "CONFIG_SCHEMA", None) - - @property - def is_multi_conf(self): - return getattr(self.module, "MULTI_CONF", False) - - @property - def to_code(self): - return getattr(self.module, "to_code", None) - - @property - def esp_platforms(self): - return getattr(self.module, "ESP_PLATFORMS", ESP_PLATFORMS) - - @property - def dependencies(self): - return getattr(self.module, "DEPENDENCIES", []) - - @property - def conflicts_with(self): - return getattr(self.module, "CONFLICTS_WITH", []) - - @property - def auto_load(self): - return getattr(self.module, "AUTO_LOAD", []) - - @property - def codeowners(self) -> List[str]: - return getattr(self.module, "CODEOWNERS", []) - - def _get_flags_set(self, name, config): - if not hasattr(self.module, name): - return set() - obj = getattr(self.module, name) - if callable(obj): - obj = obj(config) - if obj is None: - return set() - if not isinstance(obj, (list, tuple, set)): - obj = [obj] - return set(obj) - - @property - def source_files(self): - if self._is_core: - core_p = os.path.abspath(os.path.join(os.path.dirname(__file__), "core")) - source_files = core.find_source_files(os.path.join(core_p, "dummy")) - ret = {} - for f in source_files: - ret[f"esphome/core/{f}"] = os.path.join(core_p, f) - return ret - - source_files = core.find_source_files(self.module.__file__) - ret = {} - # Make paths absolute - directory = os.path.abspath(os.path.dirname(self.module.__file__)) - for x in source_files: - full_file = os.path.join(directory, x) - rel = os.path.relpath(full_file, self.base_components_path) - # Always use / for C++ include names - rel = rel.replace(os.sep, "/") - target_file = f"esphome/components/{rel}" - ret[target_file] = full_file - return ret - - -CORE_COMPONENTS_PATH = os.path.abspath( - os.path.join(os.path.dirname(__file__), "components") -) -_UNDEF = object() -CUSTOM_COMPONENTS_PATH = _UNDEF - - -def _mount_config_dir(): - global CUSTOM_COMPONENTS_PATH - if CUSTOM_COMPONENTS_PATH is not _UNDEF: - return - custom_path = os.path.abspath(os.path.join(CORE.config_dir, "custom_components")) - if not os.path.isdir(custom_path): - CUSTOM_COMPONENTS_PATH = None - return - if CORE.config_dir not in sys.path: - sys.path.insert(0, CORE.config_dir) - CUSTOM_COMPONENTS_PATH = custom_path - - -def _lookup_module(domain, is_platform): - if domain in _COMPONENT_CACHE: - return _COMPONENT_CACHE[domain] - - _mount_config_dir() - # First look for custom_components - try: - module = importlib.import_module(f"custom_components.{domain}") - except ImportError as e: - # ImportError when no such module - if "No module named" not in str(e): - _LOGGER.warning( - "Unable to import custom component %s:", domain, exc_info=True - ) - except Exception: # pylint: disable=broad-except - # Other error means component has an issue - _LOGGER.error("Unable to load custom component %s:", domain, exc_info=True) - return None - else: - # Found in custom components - manif = ComponentManifest( - module, CUSTOM_COMPONENTS_PATH, is_platform=is_platform - ) - _COMPONENT_CACHE[domain] = manif - return manif - - try: - module = importlib.import_module(f"esphome.components.{domain}") - except ImportError as e: - if "No module named" not in str(e): - _LOGGER.error("Unable to import component %s:", domain, exc_info=True) - return None - except Exception: # pylint: disable=broad-except - _LOGGER.error("Unable to load component %s:", domain, exc_info=True) - return None - else: - manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform) - _COMPONENT_CACHE[domain] = manif - return manif - - -def get_component(domain): - assert "." not in domain - return _lookup_module(domain, False) - - -def get_platform(domain, platform): - full = f"{platform}.{domain}" - return _lookup_module(full, True) - - -_COMPONENT_CACHE["esphome"] = ComponentManifest( - core_config, - CORE_COMPONENTS_PATH, - is_core=True, - is_platform=False, -) - def iter_components(config): for domain, conf in config.items(): component = get_component(domain) - if component.is_multi_conf: + if component.multi_conf: for conf_ in conf: yield domain, component, conf_ else: @@ -453,6 +292,9 @@ def recursive_check_replaceme(value): def validate_config(config, command_line_substitutions): result = Config() + loader.clear_component_meta_finders() + loader.install_custom_components_meta_finder() + # 0. Load packages if CONF_PACKAGES in config: from esphome.components.packages import do_packages_pass @@ -486,6 +328,18 @@ def validate_config(config, command_line_substitutions): except vol.Invalid as err: result.add_error(err) + # 1.2. Load external_components + if CONF_EXTERNAL_COMPONENTS in config: + from esphome.components.external_components import do_external_components_pass + + result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS) + try: + do_external_components_pass(config) + except vol.Invalid as err: + result.update(config) + result.add_error(err) + return result + if "esphomeyaml" in config: _LOGGER.warning( "The esphomeyaml section has been renamed to esphome in 1.11.0. " @@ -651,9 +505,16 @@ def validate_config(config, command_line_substitutions): ) continue - if comp.is_multi_conf: + if comp.multi_conf: if not isinstance(conf, list): result[domain] = conf = [conf] + if not isinstance(comp.multi_conf, bool) and len(conf) > comp.multi_conf: + result.add_str_error( + "Component {} supports a maximum of {} " + "entries ({} found).".format(domain, comp.multi_conf, len(conf)), + path, + ) + continue for i, part_conf in enumerate(conf): validate_queue.append((path + [i], part_conf, comp)) continue diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 4a65c59379..24c86e6713 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1556,3 +1556,17 @@ def polling_component_schema(default_update_interval): ): update_interval, } ) + + +def url(value): + import urllib.parse + + value = string_strict(value) + try: + parsed = urllib.parse.urlparse(value) + except ValueError as e: + raise Invalid("Not a valid URL") from e + + if not parsed.scheme or not parsed.netloc: + raise Invalid("Expected a URL scheme and host") + return parsed.geturl() diff --git a/esphome/const.py b/esphome/const.py index 8f9c80e2e3..a0a3ff3dcf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,8 +1,8 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 17 -PATCH_VERSION = "2" +MINOR_VERSION = 18 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" @@ -10,12 +10,14 @@ ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_-" +ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" + # Lookup table from ESP32 arduino framework version to latest platformio # package with that version # See also https://github.com/platformio/platform-espressif32/releases ARDUINO_VERSION_ESP32 = { "dev": "https://github.com/platformio/platform-espressif32.git", + "1.0.6": "platformio/espressif32@3.2.0", "1.0.5": "platformio/espressif32@3.1.1", "1.0.4": "platformio/espressif32@3.0.0", "1.0.3": "platformio/espressif32@1.10.0", @@ -56,6 +58,7 @@ CONF_ACTION_ID = "action_id" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ALPHA = "alpha" +CONF_ALTITUDE = "altitude" CONF_AND = "and" CONF_AP = "ap" CONF_ARDUINO_VERSION = "arduino_version" @@ -94,6 +97,7 @@ CONF_BUSY_PIN = "busy_pin" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" +CONF_CAPACITY = "capacity" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" CONF_CERTIFICATE = "certificate" @@ -112,6 +116,7 @@ CONF_CO2 = "co2" CONF_CODE = "code" CONF_COLD_WHITE = "cold_white" CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" +CONF_COLOR = "color" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" @@ -127,7 +132,9 @@ CONF_CONDUCTIVITY = "conductivity" CONF_CONTRAST = "contrast" CONF_COOL_ACTION = "cool_action" CONF_COOL_MODE = "cool_mode" +CONF_COUNT = "count" CONF_COUNT_MODE = "count_mode" +CONF_COURSE = "course" CONF_CRON = "cron" CONF_CS_PIN = "cs_pin" CONF_CSS_INCLUDE = "css_include" @@ -176,14 +183,17 @@ CONF_ECHO_PIN = "echo_pin" CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" +CONF_ENABLE_MDNS = "enable_mdns" CONF_ENABLE_PIN = "enable_pin" CONF_ENABLE_TIME = "enable_time" CONF_ENERGY = "energy" CONF_ENTITY_ID = "entity_id" CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" CONF_ESPHOME = "esphome" +CONF_ETHERNET = "ethernet" CONF_EVENT = "event" CONF_EXPIRE_AFTER = "expire_after" +CONF_EXTERNAL_COMPONENTS = "external_components" CONF_EXTERNAL_VCC = "external_vcc" CONF_FALLING_EDGE = "falling_edge" CONF_FAMILY = "family" @@ -204,11 +214,14 @@ CONF_FILE = "file" CONF_FILTER = "filter" CONF_FILTER_OUT = "filter_out" CONF_FILTERS = "filters" +CONF_FINGER_ID = "finger_id" +CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMAT = "format" +CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" CONF_FREQUENCY = "frequency" CONF_FROM = "from" CONF_FULL_UPDATE_EVERY = "full_update_every" @@ -270,6 +283,9 @@ CONF_KEEP_ON_TIME = "keep_on_time" CONF_KEEPALIVE = "keepalive" CONF_KEY = "key" CONF_LAMBDA = "lambda" +CONF_LAST_CONFIDENCE = "last_confidence" +CONF_LAST_FINGER_ID = "last_finger_id" +CONF_LATITUDE = "latitude" CONF_LENGTH = "length" CONF_LEVEL = "level" CONF_LG = "lg" @@ -282,6 +298,7 @@ CONF_LOCAL = "local" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" +CONF_LONGITUDE = "longitude" CONF_LOW = "low" CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference" CONF_MAC_ADDRESS = "mac_address" @@ -328,11 +345,13 @@ CONF_NAME = "name" CONF_NBITS = "nbits" CONF_NEC = "nec" CONF_NETWORKS = "networks" +CONF_NEW_PASSWORD = "new_password" CONF_NOISE_LEVEL = "noise_level" CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_CHANNELS = "num_channels" CONF_NUM_CHIPS = "num_chips" CONF_NUM_LEDS = "num_leds" +CONF_NUM_SCANS = "num_scans" CONF_NUMBER = "number" CONF_OFF_MODE = "off_mode" CONF_OFFSET = "offset" @@ -342,7 +361,14 @@ CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" +CONF_ON_CONNECT = "on_connect" +CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" +CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" +CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" +CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" +CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" +CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" CONF_ON_JSON_MESSAGE = "on_json_message" CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" @@ -380,6 +406,7 @@ CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" CONF_PASSWORD = "password" +CONF_PATH = "path" CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" @@ -441,6 +468,7 @@ CONF_RESTORE_MODE = "restore_mode" CONF_RESTORE_STATE = "restore_state" CONF_RESTORE_VALUE = "restore_value" CONF_RETAIN = "retain" +CONF_REVERSE_ACTIVE_ENERGY = "reverse_active_energy" CONF_RGB_ORDER = "rgb_order" CONF_RGBW = "rgbw" CONF_RISING_EDGE = "rising_edge" @@ -456,6 +484,7 @@ CONF_RX_ONLY = "rx_only" CONF_RX_PIN = "rx_pin" CONF_SAFE_MODE = "safe_mode" CONF_SAMSUNG = "samsung" +CONF_SATELLITES = "satellites" CONF_SCAN = "scan" CONF_SCL = "scl" CONF_SCL_PIN = "scl_pin" @@ -463,10 +492,12 @@ CONF_SDA = "sda" CONF_SDO_PIN = "sdo_pin" CONF_SECOND = "second" CONF_SECONDS = "seconds" +CONF_SECURITY_LEVEL = "security_level" CONF_SEGMENTS = "segments" CONF_SEL_PIN = "sel_pin" CONF_SEND_EVERY = "send_every" CONF_SEND_FIRST_AT = "send_first_at" +CONF_SENSING_PIN = "sensing_pin" CONF_SENSOR = "sensor" CONF_SENSOR_ID = "sensor_id" CONF_SENSORS = "sensors" @@ -485,6 +516,7 @@ CONF_SLEEP_DURATION = "sleep_duration" CONF_SLEEP_PIN = "sleep_pin" CONF_SLEEP_WHEN_DONE = "sleep_when_done" CONF_SONY = "sony" +CONF_SOURCE = "source" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_COUNT = "speed_count" @@ -496,6 +528,7 @@ CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STATE = "state" CONF_STATE_TOPIC = "state_topic" CONF_STATIC_IP = "static_ip" +CONF_STATUS = "status" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" @@ -590,8 +623,11 @@ ICON_ACCELERATION = "mdi:axis-arrow" ICON_ACCELERATION_X = "mdi:axis-x-arrow" ICON_ACCELERATION_Y = "mdi:axis-y-arrow" ICON_ACCELERATION_Z = "mdi:axis-z-arrow" +ICON_ACCOUNT = "mdi:account" +ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" +ICON_BLUETOOTH = "mdi:bluetooth" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" ICON_BUG = "mdi:bug" @@ -599,7 +635,9 @@ ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" ICON_COUNTER = "mdi:counter" ICON_CURRENT_AC = "mdi:current-ac" +ICON_DATABASE = "mdi:database" ICON_EMPTY = "" +ICON_FINGERPRINT = "mdi:fingerprint" ICON_FLASH = "mdi:flash" ICON_FLASK = "mdi:flask" ICON_FLASK_OUTLINE = "mdi:flask-outline" @@ -607,6 +645,7 @@ ICON_FLOWER = "mdi:flower" ICON_GAS_CYLINDER = "mdi:gas-cylinder" ICON_GAUGE = "mdi:gauge" ICON_GRAIN = "mdi:grain" +ICON_KEY_PLUS = "mdi:key-plus" ICON_LIGHTBULB = "mdi:lightbulb" ICON_MAGNET = "mdi:magnet" ICON_MOLECULE_CO2 = "mdi:molecule-co2" @@ -623,6 +662,7 @@ ICON_RULER = "mdi:ruler" ICON_SCALE = "mdi:scale" ICON_SCALE_BATHROOM = "mdi:scale-bathroom" ICON_SCREEN_ROTATION = "mdi:screen-rotation" +ICON_SECURITY = "mdi:security" ICON_SIGN_DIRECTION = "mdi:sign-direction" ICON_SIGNAL = "mdi:signal-distance-variant" ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal" diff --git a/esphome/core.py b/esphome/core/__init__.py similarity index 99% rename from esphome/core.py rename to esphome/core/__init__.py index cf2a07d35f..47048478ef 100644 --- a/esphome/core.py +++ b/esphome/core/__init__.py @@ -16,13 +16,14 @@ from esphome.const import ( CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, + CONF_ETHERNET, CONF_WIFI, ) from esphome.helpers import ensure_unique_string, is_hassio from esphome.util import OrderedDict if TYPE_CHECKING: - from .cpp_generator import MockObj, MockObjClass, Statement + from ..cpp_generator import MockObj, MockObjClass, Statement _LOGGER = logging.getLogger(__name__) @@ -580,8 +581,8 @@ class EsphomeCore: if "wifi" in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] - if "ethernet" in self.config: - return self.config["ethernet"][CONF_USE_ADDRESS] + if CONF_ETHERNET in self.config: + return self.config[CONF_ETHERNET][CONF_USE_ADDRESS] return None diff --git a/esphome/core/application.h b/esphome/core/application.h index e9c8638f60..aeda245161 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -39,7 +39,7 @@ class Application { public: void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { if (name_add_mac_suffix) { - this->name_ = name + "_" + get_mac_address().substr(6); + this->name_ = name + "-" + get_mac_address().substr(6); } else { this->name_ = name; } diff --git a/esphome/core_config.py b/esphome/core/config.py similarity index 98% rename from esphome/core_config.py rename to esphome/core/config.py index 55d219d86b..1dbe2ec33a 100644 --- a/esphome/core_config.py +++ b/esphome/core/config.py @@ -308,12 +308,6 @@ def to_code(config): cg.add_build_flag("-fno-exceptions") # Libraries - if CORE.is_esp32: - cg.add_library("ESPmDNS", None) - elif CORE.is_esp8266: - cg.add_library("ESP8266WiFi", None) - cg.add_library("ESP8266mDNS", None) - for lib in config[CONF_LIBRARIES]: if "@" in lib: name, vers = lib.split("@", 1) diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index a701b8013c..4e15d142be 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -16,12 +16,14 @@ #include "esphome/components/ethernet/ethernet_component.h" #endif +#ifdef USE_MDNS #ifdef ARDUINO_ARCH_ESP32 #include #endif #ifdef ARDUINO_ARCH_ESP8266 #include #endif +#endif namespace esphome { @@ -39,7 +41,7 @@ bool network_is_connected() { return false; } -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) bool mdns_setup; #endif @@ -47,6 +49,7 @@ bool mdns_setup; static const uint8_t WEBSERVER_PORT = 80; #endif +#ifdef USE_MDNS #ifdef ARDUINO_ARCH_ESP8266 void network_setup_mdns(IPAddress address, int interface) { // Latest arduino framework breaks mDNS for AP interface @@ -80,8 +83,10 @@ void network_setup_mdns(IPAddress address, int interface) { MDNS.addService("prometheus-http", "tcp", WEBSERVER_PORT); #endif } +#endif + void network_tick_mdns() { -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) if (mdns_setup) MDNS.update(); #endif diff --git a/esphome/core/util.h b/esphome/core/util.h index 0e121ef382..8e30211be6 100644 --- a/esphome/core/util.h +++ b/esphome/core/util.h @@ -17,6 +17,7 @@ void network_setup_mdns(IPAddress address, int interface); #ifdef ARDUINO_ARCH_ESP32 void network_setup_mdns(); #endif + void network_tick_mdns(); } // namespace esphome diff --git a/esphome/dashboard/static/js/esphome.js b/esphome/dashboard/static/js/esphome.js index 52dda446ce..e861f169df 100644 --- a/esphome/dashboard/static/js/esphome.js +++ b/esphome/dashboard/static/js/esphome.js @@ -1012,6 +1012,10 @@ jQuery.validator.addMethod("nospaces", (value, element) => { return value.indexOf(' ') < 0; }, "Name cannot contain any spaces!"); +jQuery.validator.addMethod("nounderscores", (value, element) => { + return value.indexOf('_') < 0; +}, "Name cannot contain underscores!"); + jQuery.validator.addMethod("lowercase", (value, element) => { return value === value.toLowerCase(); }, "Name must be all lower case!"); diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 142bc2cd6f..b10901f5d4 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -359,12 +359,12 @@

Names must be all lowercase and must not contain any spaces! Characters that are allowed are: a-z, - 0-9, _ and -. + 0-9 and -.

+ data-rule-lowercase="true" data-rule-nounderscores="true" required>
@@ -649,7 +649,7 @@