diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93a29874f7..9473dc87dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,26 +51,26 @@ jobs: name: Run script/clang-format - id: clang-tidy name: Run script/clang-tidy for ESP8266 - options: --environment esp8266-tidy --grep USE_ESP8266 + options: --environment esp8266-arduino-tidy --grep USE_ESP8266 pio_cache_key: tidyesp8266 - id: clang-tidy - name: Run script/clang-tidy for ESP32 1/4 - options: --environment esp32-tidy --split-num 4 --split-at 1 + name: Run script/clang-tidy for ESP32 Arduino 1/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 1 pio_cache_key: tidyesp32 - id: clang-tidy - name: Run script/clang-tidy for ESP32 2/4 - options: --environment esp32-tidy --split-num 4 --split-at 2 + name: Run script/clang-tidy for ESP32 Arduino 2/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 2 pio_cache_key: tidyesp32 - id: clang-tidy - name: Run script/clang-tidy for ESP32 3/4 - options: --environment esp32-tidy --split-num 4 --split-at 3 + name: Run script/clang-tidy for ESP32 Arduino 3/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 3 pio_cache_key: tidyesp32 - id: clang-tidy - name: Run script/clang-tidy for ESP32 4/4 - options: --environment esp32-tidy --split-num 4 --split-at 4 + name: Run script/clang-tidy for ESP32 Arduino 4/4 + options: --environment esp32-arduino-tidy --split-num 4 --split-at 4 pio_cache_key: tidyesp32 - id: clang-tidy - name: Run script/clang-tidy for ESP32 esp-idf + name: Run script/clang-tidy for ESP32 IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF pio_cache_key: tidyesp32-idf diff --git a/CODEOWNERS b/CODEOWNERS index 6dbdef12ec..351d9f5fc9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,13 +28,16 @@ esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bl0940/* @tobias- esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth +esphome/components/bmp3xx/* @martgras esphome/components/button/* @esphome/core esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @MrEditor97 esphome/components/captive_portal/* @OttoWinter esphome/components/ccs811/* @habbie +esphome/components/cd74hc4067/* @asoehlke esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz @@ -55,6 +58,7 @@ esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_camera_web_server/* @ayufan +esphome/components/esp32_can/* @Sympatron esphome/components/esp32_improv/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/exposure_notifications/* @OttoWinter @@ -65,6 +69,7 @@ esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/growatt_solar/* @leeuwte esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann @@ -74,11 +79,13 @@ esphome/components/homeassistant/* @OttoWinter esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core esphome/components/improv_serial/* @esphome/core +esphome/components/ina260/* @MrEditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate6/* @jesserockz esphome/components/integration/* @OttoWinter esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter +esphome/components/kalman_combinator/* @Cat-Ion esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/logger/* @esphome/core @@ -92,10 +99,13 @@ esphome/components/mcp23x08_base/* @jesserockz esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho +esphome/components/mcp3204/* @rsumner +esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/midea/* @dudanov +esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras @@ -123,6 +133,7 @@ esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core +esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/rc522/* @glmnet @@ -132,7 +143,7 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet -esphome/components/safe_mode/* @paulmonigatti +esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/scd4x/* @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces @@ -142,7 +153,7 @@ esphome/components/select/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny -esphome/components/shutdown/* @esphome/core +esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/socket/* @esphome/core @@ -179,6 +190,7 @@ esphome/components/toshiba/* @kbx81 esphome/components/tsl2591/* @wjcarpenter esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz +esphome/components/tuya/number/* @frankiboy1 esphome/components/tuya/sensor/* @jesserockz esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra diff --git a/docker/Dockerfile b/docker/Dockerfile index 25f2cf85d2..330901a776 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,12 +5,12 @@ # One of "docker", "hassio" ARG BASEIMGTYPE=docker -FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.1 AS base-hassio-amd64 -FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.1 AS base-hassio-arm64 -FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.1 AS base-hassio-armv7 -FROM debian:bullseye-20211011-slim AS base-docker-amd64 -FROM debian:bullseye-20211011-slim AS base-docker-arm64 -FROM debian:bullseye-20211011-slim AS base-docker-armv7 +FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 +FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 +FROM debian:bullseye-20211220-slim AS base-docker-amd64 +FROM debian:bullseye-20211220-slim AS base-docker-arm64 +FROM debian:bullseye-20211220-slim AS base-docker-armv7 # Use TARGETARCH/TARGETVARIANT defined by docker # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope @@ -42,8 +42,8 @@ ENV \ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ - wheel==0.36.2 \ - platformio==5.2.2 \ + wheel==0.37.1 \ + platformio==5.2.4 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_libraries_interval 1000000 \ @@ -64,7 +64,7 @@ RUN \ # Copy esphome and install COPY . /esphome -RUN pip3 install --no-cache-dir /esphome +RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome # Settings for dashboard ENV USERNAME="" PASSWORD="" @@ -112,7 +112,7 @@ RUN \ # Copy esphome and install COPY . /esphome -RUN pip3 install --no-cache-dir /esphome +RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome # Labels LABEL \ diff --git a/docker/build.py b/docker/build.py index 1157d8287a..d5926ae3d4 100755 --- a/docker/build.py +++ b/docker/build.py @@ -32,6 +32,7 @@ parser.add_argument("--dry-run", action="store_true", help="Don't run any comman subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) build_parser = subparsers.add_parser("build", help="Build the image") build_parser.add_argument("--push", help="Also push the images", action="store_true") +build_parser.add_argument("--load", help="Load the docker image locally", action="store_true") manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") @@ -132,6 +133,8 @@ def main(): cmd += ["--tag", img] if args.push: cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"] + if args.load: + cmd += ["--load"] run_command(*cmd, ".") elif args.command == "manifest": diff --git a/esphome/__main__.py b/esphome/__main__.py index 2f06a71b5f..6f57791480 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -145,6 +145,8 @@ def wrap_to_code(name, comp): if comp.config_schema is not None: conf_str = yaml_util.dump(conf) conf_str = conf_str.replace("//", "") + # remove tailing \ to avoid multi-line comment warning + conf_str = conf_str.replace("\\\n", "\n") cg.add(cg.LineComment(indent(conf_str))) await coro(conf) diff --git a/esphome/codegen.py b/esphome/codegen.py index 5e1e934e58..3ea3df8706 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -75,8 +75,7 @@ from esphome.cpp_types import ( # noqa optional, arduino_json_ns, JsonObject, - JsonObjectRef, - JsonObjectConstRef, + JsonObjectConst, Controller, GPIOPin, InternalGPIOPin, diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f615815023..1f629c2c85 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -20,6 +20,7 @@ namespace esphome { namespace api { static const char *const TAG = "api.connection"; +static const int ESP32_CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { @@ -704,7 +705,9 @@ void APIConnection::send_camera_state(std::shared_ptr return; if (this->image_reader_.available()) return; - this->image_reader_.set_image(std::move(image)); + if (image->was_requested_by(esphome::esp32_camera::API_REQUESTER) || + image->was_requested_by(esphome::esp32_camera::IDLE)) + this->image_reader_.set_image(std::move(image)); } bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { ListEntitiesCameraResponse msg; @@ -722,9 +725,14 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { return; if (msg.single) - esp32_camera::global_esp32_camera->request_image(); - if (msg.stream) - esp32_camera::global_esp32_camera->request_stream(); + esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::API_REQUESTER); + if (msg.stream) { + esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::API_REQUESTER); + + App.scheduler.set_timeout(this->parent_, "api_esp32_camera_stop_stream", ESP32_CAMERA_STOP_STREAM, []() { + esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::API_REQUESTER); + }); + } } #endif diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 23766ec1b1..094dd67e33 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -252,7 +252,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // uncomment for even more debugging #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); #endif frame->msg = std::move(rx_buf_); // consume msg @@ -546,7 +546,8 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { size_t total_write_len = 0; for (int i = 0; i < iovcnt; i++) { #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); + ESP_LOGVV(TAG, "Sending raw: %s", + format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); #endif total_write_len += iov[i].iov_len; } @@ -720,7 +721,7 @@ APIError APINoiseFrameHelper::shutdown(int how) { } extern "C" { // declare how noise generates random bytes (here with a good HWRNG based on the RF system) -void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast(output), len); } +void noise_rand_bytes(void *output, size_t len) { esphome::random_bytes(reinterpret_cast(output), len); } } #endif // USE_API_NOISE @@ -855,7 +856,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // uncomment for even more debugging #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); #endif frame->msg = std::move(rx_buf_); // consume msg @@ -934,7 +935,8 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt size_t total_write_len = 0; for (int i = 0; i < iovcnt; i++) { #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); + ESP_LOGVV(TAG, "Sending raw: %s", + format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); #endif total_write_len += iov[i].iov_len; } diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 26269bcae4..90cfe751b6 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -12,10 +12,10 @@ template class TemplatableStringValue : public TemplatableValue() {} - template::value, int> = 0> + template::value, int> = 0> TemplatableStringValue(F value) : TemplatableValue(value) {} - template::value, int> = 0> + template::value, int> = 0> TemplatableStringValue(F f) : TemplatableValue([f](X... x) -> std::string { return to_string(f(x...)); }) {} }; diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index 42c30598ad..9d550fcf8c 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -45,6 +45,8 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device this->battery_voltage_->publish_state(*res->battery_voltage); success = true; } + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(device.get_rssi()); return success; } diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index ca079bf8c1..9398c02bcf 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -28,6 +28,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + void set_signal_strength(sensor::Sensor *signal_strength) { signal_strength_ = signal_strength; } protected: uint64_t address_; @@ -35,6 +36,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *humidity_{nullptr}; sensor::Sensor *battery_level_{nullptr}; sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index bde83c28b6..7baab51944 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -6,15 +6,18 @@ from esphome.const import ( CONF_BATTERY_VOLTAGE, CONF_MAC_ADDRESS, CONF_HUMIDITY, + CONF_SIGNAL_STRENGTH, CONF_TEMPERATURE, CONF_ID, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, UNIT_PERCENT, UNIT_VOLT, ) @@ -59,6 +62,13 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) @@ -85,3 +95,6 @@ async def to_code(config): if CONF_BATTERY_VOLTAGE in config: sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) cg.add(var.set_battery_voltage(sens)) + if CONF_SIGNAL_STRENGTH in config: + sens = await sensor.new_sensor(config[CONF_SIGNAL_STRENGTH]) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 5645f46f1c..4a95f8c339 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -80,21 +80,23 @@ void BangBangClimate::compute_state_() { climate::ClimateAction target_action; if (too_cold) { - // too cold -> enable heating if possible, else idle - if (this->supports_heat_) + // too cold -> enable heating if possible and enabled, else idle + if (this->supports_heat_ && + (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_HEAT)) target_action = climate::CLIMATE_ACTION_HEATING; else target_action = climate::CLIMATE_ACTION_IDLE; } else if (too_hot) { - // too hot -> enable cooling if possible, else idle - if (this->supports_cool_) + // too hot -> enable cooling if possible and enabled, else idle + if (this->supports_cool_ && + (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_COOL)) target_action = climate::CLIMATE_ACTION_COOLING; else target_action = climate::CLIMATE_ACTION_IDLE; } else { // neither too hot nor too cold -> in range - if (this->supports_cool_ && this->supports_heat_) { - // if supports both ends, go to idle action + if (this->supports_cool_ && this->supports_heat_ && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { + // if supports both ends and both cooling and heating enabled, go to idle action target_action = climate::CLIMATE_ACTION_IDLE; } else { // else use current mode and don't change (hysteresis) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 41da83aa3e..71422609d7 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -48,7 +48,10 @@ void BinarySensor::set_device_class(const std::string &device_class) { this->dev std::string BinarySensor::get_device_class() { if (this->device_class_.has_value()) return *this->device_class_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->device_class(); +#pragma GCC diagnostic pop } void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 9c0d43fa98..ecf68de74c 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -75,6 +75,7 @@ class BinarySensor : public EntityBase { // ========== OVERRIDE METHODS ========== // (You'll only need this when creating your own custom binary sensor) /// Get the default device class for this sensor, or empty string for no default. + ESPDEPRECATED("device_class() is deprecated, set property during config validation instead.", "2022.01") virtual std::string device_class(); protected: diff --git a/esphome/components/bl0940/__init__.py b/esphome/components/bl0940/__init__.py new file mode 100644 index 0000000000..087626a4e7 --- /dev/null +++ b/esphome/components/bl0940/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@tobias-"] diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp new file mode 100644 index 0000000000..19672e98d0 --- /dev/null +++ b/esphome/components/bl0940/bl0940.cpp @@ -0,0 +1,137 @@ +#include "bl0940.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940"; + +static const uint8_t BL0940_READ_COMMAND = 0x50; // 0x58 according to documentation +static const uint8_t BL0940_FULL_PACKET = 0xAA; +static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to documentation + +static const uint8_t BL0940_WRITE_COMMAND = 0xA0; // 0xA8 according to documentation +static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; +static const uint8_t BL0940_REG_MODE = 0x18; +static const uint8_t BL0940_REG_SOFT_RESET = 0x19; +static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; +static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; + +const uint8_t BL0940_INIT[5][6] = { + // Reset to default + {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, + // Enable User Operation Write + {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, + // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS + {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, + // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS + {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; + +void BL0940::loop() { + DataPacket buffer; + if (!this->available()) { + return; + } + if (read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (validate_checksum(&buffer)) { + received_package_(&buffer); + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + } +} + +bool BL0940::validate_checksum(const DataPacket *data) { + uint8_t checksum = BL0940_READ_COMMAND; + // Whole package but checksum + for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { + checksum += data->raw[i]; + } + checksum ^= 0xFF; + if (checksum != data->checksum) { + ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + } + return checksum == data->checksum; +} + +void BL0940::update() { + this->flush(); + this->write_byte(BL0940_READ_COMMAND); + this->write_byte(BL0940_FULL_PACKET); +} + +void BL0940::setup() { + for (auto i : BL0940_INIT) { + this->write_array(i, 6); + delay(1); + } + this->flush(); +} + +float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { + auto tb = (float) (temperature.h << 8 | temperature.l); + float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; + if (sensor != nullptr) { + if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { + ESP_LOGD("bl0940", "Invalid temperature change. Sensor: '%s', Old temperature: %f, New temperature: %f", + sensor->get_name().c_str(), sensor->get_state(), converted_temp); + return 0.0f; + } + sensor->publish_state(converted_temp); + } + return converted_temp; +} + +void BL0940::received_package_(const DataPacket *data) const { + // Bad header + if (data->frame_header != BL0940_PACKET_HEADER) { + ESP_LOGI("bl0940", "Invalid data. Header mismatch: %d", data->frame_header); + return; + } + + float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; + float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; + float watt = (float) to_int32_t(data->watt) / power_reference_; + uint32_t cf_cnt = to_uint32_t(data->cf_cnt); + float total_energy_consumption = (float) cf_cnt / energy_reference_; + + float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); + float tps2 = update_temp_(external_temperature_sensor_, data->tps2); + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(v_rms); + } + if (current_sensor_ != nullptr) { + current_sensor_->publish_state(i_rms); + } + if (power_sensor_ != nullptr) { + power_sensor_->publish_state(watt); + } + if (energy_sensor_ != nullptr) { + energy_sensor_->publish_state(total_energy_consumption); + } + + ESP_LOGV("bl0940", "BL0940: U %fV, I %fA, P %fW, Cnt %d, ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, + total_energy_consumption, tps1, tps2); +} + +void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexity) + ESP_LOGCONFIG(TAG, "BL0940:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Energy", this->energy_sensor_); + LOG_SENSOR("", "Internal temperature", this->internal_temperature_sensor_); + LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); +} + +uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h new file mode 100644 index 0000000000..49c8e50595 --- /dev/null +++ b/esphome/components/bl0940/bl0940.h @@ -0,0 +1,109 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bl0940 { + +static const float BL0940_PREF = 1430; +static const float BL0940_UREF = 33000; +static const float BL0940_IREF = 275000; // 2750 from tasmota. Seems to generate values 100 times too high + +// Measured to 297J per click according to power consumption of 5 minutes +// Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 +static const float BL0940_EREF = 3.6e6 / 297; + +struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + uint8_t h; +} __attribute__((packed)); + +struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t h; +} __attribute__((packed)); + +struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + int8_t h; +} __attribute__((packed)); + +// Caveat: All these values are big endian (low - middle - high) + +union DataPacket { // NOLINT(altera-struct-pack-align) + uint8_t raw[35]; + struct { + uint8_t frame_header; // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. + ube24_t i_fast_rms; // 0x00 + ube24_t i_rms; // 0x04 + ube24_t RESERVED0; // reserved + ube24_t v_rms; // 0x06 + ube24_t RESERVED1; // reserved + sbe24_t watt; // 0x08 + ube24_t RESERVED2; // reserved + ube24_t cf_cnt; // 0x0A + ube24_t RESERVED3; // reserved + ube16_t tps1; // 0x0c + uint8_t RESERVED4; // value of 0x00 + ube16_t tps2; // 0x0c + uint8_t RESERVED5; // value of 0x00 + uint8_t checksum; // checksum + }; +} __attribute__((packed)); + +class BL0940 : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { + internal_temperature_sensor_ = internal_temperature_sensor; + } + void set_external_temperature_sensor(sensor::Sensor *external_temperature_sensor) { + external_temperature_sensor_ = external_temperature_sensor; + } + + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + // NB This may be negative as the circuits is seemingly able to measure + // power in both directions + sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; + sensor::Sensor *internal_temperature_sensor_; + sensor::Sensor *external_temperature_sensor_; + + // Max difference between two measurements of the temperature. Used to avoid noise. + float max_temperature_diff_{0}; + // Divide by this to turn into Watt + float power_reference_ = BL0940_PREF; + // Divide by this to turn into Volt + float voltage_reference_ = BL0940_UREF; + // Divide by this to turn into Ampere + float current_reference_ = BL0940_IREF; + // Divide by this to turn into kWh + float energy_reference_ = BL0940_EREF; + + float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; + + static uint32_t to_uint32_t(ube24_t input); + + static int32_t to_int32_t(sbe24_t input); + + static bool validate_checksum(const DataPacket *data); + + void received_package_(const DataPacket *data) const; +}; +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py new file mode 100644 index 0000000000..ce630b7408 --- /dev/null +++ b/esphome/components/bl0940/sensor.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_CURRENT, + CONF_ENERGY, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_TEMPERATURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, +) + +DEPENDENCIES = ["uart"] + +CONF_INTERNAL_TEMPERATURE = "internal_temperature" +CONF_EXTERNAL_TEMPERATURE = "external_temperature" + +bl0940_ns = cg.esphome_ns.namespace("bl0940") +BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0940), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + UNIT_AMPERE, + ICON_EMPTY, + 2, + DEVICE_CLASS_CURRENT, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 0, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_EMPTY, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_EMPTY, + 0, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_NONE, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) + if CONF_INTERNAL_TEMPERATURE in config: + conf = config[CONF_INTERNAL_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_internal_temperature_sensor(sens)) + if CONF_EXTERNAL_TEMPERATURE in config: + conf = config[CONF_EXTERNAL_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_external_temperature_sensor(sens)) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index 627072443e..d3a228328b 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -201,7 +201,7 @@ void BME280Component::update() { float pressure = this->read_pressure_(data, t_fine); float humidity = this->read_humidity_(data, t_fine); - ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); + ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) diff --git a/esphome/components/bmp3xx/__init__.py b/esphome/components/bmp3xx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp3xx/bmp3xx.cpp b/esphome/components/bmp3xx/bmp3xx.cpp new file mode 100644 index 0000000000..410b7a3173 --- /dev/null +++ b/esphome/components/bmp3xx/bmp3xx.cpp @@ -0,0 +1,388 @@ +/* + based on BMP388_DEV by Martin Lindupp + under MIT License (MIT) + Copyright (C) Martin Lindupp 2020 + http://github.com/MartinL1/BMP388_DEV +*/ + +#include "bmp3xx.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace bmp3xx { + +static const char *const TAG = "bmp3xx.sensor"; + +static const LogString *chip_type_to_str(uint8_t chip_type) { + switch (chip_type) { + case BMP388_ID: + return LOG_STR("BMP 388"); + case BMP390_ID: + return LOG_STR("BMP 390"); + default: + return LOG_STR("Unknown Chip Type"); + } +} + +static const LogString *oversampling_to_str(Oversampling oversampling) { + switch (oversampling) { + case Oversampling::OVERSAMPLING_NONE: + return LOG_STR("None"); + case Oversampling::OVERSAMPLING_X2: + return LOG_STR("2x"); + case Oversampling::OVERSAMPLING_X4: + return LOG_STR("4x"); + case Oversampling::OVERSAMPLING_X8: + return LOG_STR("8x"); + case Oversampling::OVERSAMPLING_X16: + return LOG_STR("16x"); + case Oversampling::OVERSAMPLING_X32: + return LOG_STR("32x"); + default: + return LOG_STR(""); + } +} + +static const LogString *iir_filter_to_str(IIRFilter filter) { + switch (filter) { + case IIRFilter::IIR_FILTER_OFF: + return LOG_STR("OFF"); + case IIRFilter::IIR_FILTER_2: + return LOG_STR("2x"); + case IIRFilter::IIR_FILTER_4: + return LOG_STR("4x"); + case IIRFilter::IIR_FILTER_8: + return LOG_STR("8x"); + case IIRFilter::IIR_FILTER_16: + return LOG_STR("16x"); + case IIRFilter::IIR_FILTER_32: + return LOG_STR("32x"); + case IIRFilter::IIR_FILTER_64: + return LOG_STR("64x"); + case IIRFilter::IIR_FILTER_128: + return LOG_STR("128x"); + default: + return LOG_STR(""); + } +} + +void BMP3XXComponent::setup() { + this->error_code_ = NONE; + ESP_LOGCONFIG(TAG, "Setting up BMP3XX..."); + // Call the Device base class "initialise" function + if (!reset()) { + ESP_LOGE(TAG, "Failed to reset BMP3XX..."); + this->error_code_ = ERROR_SENSOR_RESET; + this->mark_failed(); + } + + if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) { + ESP_LOGE(TAG, "Can't read chip id"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); + + if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) { + ESP_LOGE(TAG, "Unknown chip id - is this really a BMP388 or BMP390?"); + this->error_code_ = ERROR_WRONG_CHIP_ID; + this->mark_failed(); + return; + } + // set sensor in sleep mode + stop_conversion(); + // Read the calibration parameters into the params structure + if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) { + ESP_LOGE(TAG, "Can't read calibration data"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + compensation_float_params_.param_T1 = + (float) compensation_params_.param_T1 / powf(2.0f, -8.0f); // Calculate the floating point trim parameters + compensation_float_params_.param_T2 = (float) compensation_params_.param_T2 / powf(2.0f, 30.0f); + compensation_float_params_.param_T3 = (float) compensation_params_.param_T3 / powf(2.0f, 48.0f); + compensation_float_params_.param_P1 = ((float) compensation_params_.param_P1 - powf(2.0f, 14.0f)) / powf(2.0f, 20.0f); + compensation_float_params_.param_P2 = ((float) compensation_params_.param_P2 - powf(2.0f, 14.0f)) / powf(2.0f, 29.0f); + compensation_float_params_.param_P3 = (float) compensation_params_.param_P3 / powf(2.0f, 32.0f); + compensation_float_params_.param_P4 = (float) compensation_params_.param_P4 / powf(2.0f, 37.0f); + compensation_float_params_.param_P5 = (float) compensation_params_.param_P5 / powf(2.0f, -3.0f); + compensation_float_params_.param_P6 = (float) compensation_params_.param_P6 / powf(2.0f, 6.0f); + compensation_float_params_.param_P7 = (float) compensation_params_.param_P7 / powf(2.0f, 8.0f); + compensation_float_params_.param_P8 = (float) compensation_params_.param_P8 / powf(2.0f, 15.0f); + compensation_float_params_.param_P9 = (float) compensation_params_.param_P9 / powf(2.0f, 48.0f); + compensation_float_params_.param_P10 = (float) compensation_params_.param_P10 / powf(2.0f, 48.0f); + compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f); + + // Initialise the BMP388 IIR filter register + if (!set_iir_filter(this->iir_filter_)) { + ESP_LOGE(TAG, "Failed to set IIR filter"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + // Set power control registers + pwr_ctrl_.bit.press_en = 1; + pwr_ctrl_.bit.temp_en = 1; + // Disable pressure if no sensor defined + // keep temperature enabled since it's needed for compensation + if (this->pressure_sensor_ == nullptr) { + pwr_ctrl_.bit.press_en = 0; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + } + // just disable oeversampling for temp if not used + if (this->temperature_sensor_ == nullptr) { + this->temperature_oversampling_ = OVERSAMPLING_NONE; + } + // Initialise the BMP388 oversampling register + if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) { + ESP_LOGE(TAG, "Failed to set oversampling register"); + this->error_code_ = ERROR_COMMUNICATION_FAILED; + this->mark_failed(); + return; + } +} + +void BMP3XXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BMP3XX:"); + ESP_LOGCONFIG(TAG, " Type: %s (0x%X)", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case NONE: + break; + case ERROR_COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with BMP3XX failed!"); + break; + case ERROR_WRONG_CHIP_ID: + ESP_LOGE( + TAG, + "BMP3XX has wrong chip ID (reported id: 0x%X) - please check if you are really using a BMP 388 or BMP 390", + this->chip_id_.reg); + break; + case ERROR_SENSOR_RESET: + ESP_LOGE(TAG, "BMP3XX failed to reset"); + break; + default: + ESP_LOGE(TAG, "BMP3XX error code %d", (int) this->error_code_); + break; + } + ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_))); + LOG_UPDATE_INTERVAL(this); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); + } +} +float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } + +inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } + +void BMP3XXComponent::update() { + // Enable sensor + ESP_LOGV(TAG, "Sending conversion request..."); + float meas_time = 1.0f; + // Ref: https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp390-ds002.pdf 3.9.2 + meas_time += 2.02f * oversampling_to_time(this->temperature_oversampling_) + 0.163f; + meas_time += 2.02f * oversampling_to_time(this->pressure_oversampling_) + 0.392f; + meas_time += 0.234f; + if (!set_mode(FORCED_MODE)) { + ESP_LOGE(TAG, "Failed start forced mode"); + this->mark_failed(); + return; + } + + ESP_LOGVV(TAG, "measurement time %d", uint32_t(ceilf(meas_time))); + this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { + float temperature = 0.0f; + float pressure = 0.0f; + if (this->pressure_sensor_ != nullptr) { + if (!get_measurements(temperature, pressure)) { + ESP_LOGW(TAG, "Failed to read pressure and temperature - skipping update"); + this->status_set_warning(); + return; + } + ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); + } else { + if (!get_temperature(temperature)) { + ESP_LOGW(TAG, "Failed to read temperature - skipping update"); + this->status_set_warning(); + return; + } + ESP_LOGD(TAG, "Got temperature=%.1f°C", temperature); + } + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + set_mode(SLEEP_MODE); + }); +} + +// Reset the BMP3XX +uint8_t BMP3XXComponent::reset() { + write_byte(BMP388_CMD, RESET_CODE); // Write the reset code to the command register + // Wait for 10ms + delay(10); + this->read_byte(BMP388_EVENT, &event_.reg); // Read the BMP388's event register + return event_.bit.por_detected; // Return if device reset is complete +} + +// Start a one shot measurement in FORCED_MODE +bool BMP3XXComponent::start_forced_conversion() { + // Only set FORCED_MODE if we're already in SLEEP_MODE + if (pwr_ctrl_.bit.mode == SLEEP_MODE) { + return set_mode(FORCED_MODE); + } + return true; +} + +// Stop the conversion and return to SLEEP_MODE +bool BMP3XXComponent::stop_conversion() { return set_mode(SLEEP_MODE); } + +// Set the pressure oversampling rate +bool BMP3XXComponent::set_pressure_oversampling(Oversampling oversampling) { + osr_.bit.osr_p = oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Set the temperature oversampling rate +bool BMP3XXComponent::set_temperature_oversampling(Oversampling oversampling) { + osr_.bit.osr_t = oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Set the IIR filter setting +bool BMP3XXComponent::set_iir_filter(IIRFilter iir_filter) { + config_.bit.iir_filter = iir_filter; + return this->write_byte(BMP388_CONFIG, config_.reg); +} + +// Get temperature +bool BMP3XXComponent::get_temperature(float &temperature) { + // Check if a measurement is ready + if (!data_ready()) { + return false; + } + uint8_t data[3]; + // Read the temperature + if (!this->read_bytes(BMP388_DATA_3, &data[0], 3)) { + ESP_LOGE(TAG, "Failed to read temperature"); + return false; + } + // Copy the temperature data into the adc variables + int32_t adc_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + // Temperature compensation (function from BMP388 datasheet) + temperature = bmp388_compensate_temperature_((float) adc_temp); + return true; +} + +// Get the pressure +bool BMP3XXComponent::get_pressure(float &pressure) { + float temperature; + return get_measurements(temperature, pressure); +} + +// Get temperature and pressure +bool BMP3XXComponent::get_measurements(float &temperature, float &pressure) { + // Check if a measurement is ready + if (!data_ready()) { + ESP_LOGD(TAG, "BMP3XX Get measurement - data not ready skipping update"); + return false; + } + + uint8_t data[6]; + // Read the temperature and pressure data + if (!this->read_bytes(BMP388_DATA_0, &data[0], 6)) { + ESP_LOGE(TAG, "Failed to read measurements"); + return false; + } + // Copy the temperature and pressure data into the adc variables + int32_t adc_pres = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t adc_temp = (int32_t) data[5] << 16 | (int32_t) data[4] << 8 | (int32_t) data[3]; + + // Temperature compensation (function from BMP388 datasheet) + temperature = bmp388_compensate_temperature_((float) adc_temp); + // Pressure compensation (function from BMP388 datasheet) + pressure = bmp388_compensate_pressure_((float) adc_pres, temperature); + // Calculate the pressure in millibar/hPa + pressure /= 100.0f; + return true; +} + +// Set the BMP388's mode in the power control register +bool BMP3XXComponent::set_mode(OperationMode mode) { + pwr_ctrl_.bit.mode = mode; + return this->write_byte(BMP388_PWR_CTRL, pwr_ctrl_.reg); +} + +// Set the BMP388 oversampling register +bool BMP3XXComponent::set_oversampling_register(Oversampling pressure_oversampling, + Oversampling temperature_oversampling) { + osr_.reg = temperature_oversampling << 3 | pressure_oversampling; + return this->write_byte(BMP388_OSR, osr_.reg); +} + +// Check if measurement data is ready +bool BMP3XXComponent::data_ready() { + // If we're in SLEEP_MODE return immediately + if (pwr_ctrl_.bit.mode == SLEEP_MODE) { + ESP_LOGD(TAG, "Not ready - sensor is in sleep mode"); + return false; + } + // Read the interrupt status register + uint8_t status; + if (!this->read_byte(BMP388_INT_STATUS, &status)) { + ESP_LOGE(TAG, "Failed to read status register"); + return false; + } + int_status_.reg = status; + ESP_LOGVV(TAG, "data ready status %d", status); + // If we're in FORCED_MODE switch back to SLEEP_MODE + if (int_status_.bit.drdy) { + if (pwr_ctrl_.bit.mode == FORCED_MODE) { + pwr_ctrl_.bit.mode = SLEEP_MODE; + } + return true; // The measurement is ready + } + return false; // The measurement is still pending +} + +//////////////////////////////////////////////////////////////////////////////// +// Bosch BMP3XXComponent (Private) Member Functions +//////////////////////////////////////////////////////////////////////////////// + +float BMP3XXComponent::bmp388_compensate_temperature_(float uncomp_temp) { + float partial_data1 = uncomp_temp - compensation_float_params_.param_T1; + float partial_data2 = partial_data1 * compensation_float_params_.param_T2; + return partial_data2 + partial_data1 * partial_data1 * compensation_float_params_.param_T3; +} + +float BMP3XXComponent::bmp388_compensate_pressure_(float uncomp_press, float t_lin) { + float partial_data1 = compensation_float_params_.param_P6 * t_lin; + float partial_data2 = compensation_float_params_.param_P7 * t_lin * t_lin; + float partial_data3 = compensation_float_params_.param_P8 * t_lin * t_lin * t_lin; + float partial_out1 = compensation_float_params_.param_P5 + partial_data1 + partial_data2 + partial_data3; + partial_data1 = compensation_float_params_.param_P2 * t_lin; + partial_data2 = compensation_float_params_.param_P3 * t_lin * t_lin; + partial_data3 = compensation_float_params_.param_P4 * t_lin * t_lin * t_lin; + float partial_out2 = + uncomp_press * (compensation_float_params_.param_P1 + partial_data1 + partial_data2 + partial_data3); + partial_data1 = uncomp_press * uncomp_press; + partial_data2 = compensation_float_params_.param_P9 + compensation_float_params_.param_P10 * t_lin; + partial_data3 = partial_data1 * partial_data2; + float partial_data4 = + partial_data3 + uncomp_press * uncomp_press * uncomp_press * compensation_float_params_.param_P11; + return partial_out1 + partial_out2 + partial_data4; +} + +} // namespace bmp3xx +} // namespace esphome diff --git a/esphome/components/bmp3xx/bmp3xx.h b/esphome/components/bmp3xx/bmp3xx.h new file mode 100644 index 0000000000..ab20abfe9b --- /dev/null +++ b/esphome/components/bmp3xx/bmp3xx.h @@ -0,0 +1,237 @@ +/* + based on BMP388_DEV by Martin Lindupp + under MIT License (MIT) + Copyright (C) Martin Lindupp 2020 + http://github.com/MartinL1/BMP388_DEV +*/ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bmp3xx { + +static const uint8_t BMP388_ID = 0x50; // The BMP388 device ID +static const uint8_t BMP390_ID = 0x60; // The BMP390 device ID +static const uint8_t RESET_CODE = 0xB6; // The BMP388 reset code + +/// BMP388_DEV Registers +enum { + BMP388_CHIP_ID = 0x00, // Chip ID register sub-address + BMP388_ERR_REG = 0x02, // Error register sub-address + BMP388_STATUS = 0x03, // Status register sub-address + BMP388_DATA_0 = 0x04, // Pressure eXtended Least Significant Byte (XLSB) register sub-address + BMP388_DATA_1 = 0x05, // Pressure Least Significant Byte (LSB) register sub-address + BMP388_DATA_2 = 0x06, // Pressure Most Significant Byte (MSB) register sub-address + BMP388_DATA_3 = 0x07, // Temperature eXtended Least Significant Byte (XLSB) register sub-address + BMP388_DATA_4 = 0x08, // Temperature Least Significant Byte (LSB) register sub-address + BMP388_DATA_5 = 0x09, // Temperature Most Significant Byte (MSB) register sub-address + BMP388_SENSORTIME_0 = 0x0C, // Sensor time register 0 sub-address + BMP388_SENSORTIME_1 = 0x0D, // Sensor time register 1 sub-address + BMP388_SENSORTIME_2 = 0x0E, // Sensor time register 2 sub-address + BMP388_EVENT = 0x10, // Event register sub-address + BMP388_INT_STATUS = 0x11, // Interrupt Status register sub-address + BMP388_INT_CTRL = 0x19, // Interrupt Control register sub-address + BMP388_IF_CONFIG = 0x1A, // Interface Configuration register sub-address + BMP388_PWR_CTRL = 0x1B, // Power Control register sub-address + BMP388_OSR = 0x1C, // Oversampling register sub-address + BMP388_ODR = 0x1D, // Output Data Rate register sub-address + BMP388_CONFIG = 0x1F, // Configuration register sub-address + BMP388_TRIM_PARAMS = 0x31, // Trim parameter registers' base sub-address + BMP388_CMD = 0x7E // Command register sub-address +}; + +/// Device mode bitfield in the control and measurement register +enum OperationMode { SLEEP_MODE = 0x00, FORCED_MODE = 0x01, NORMAL_MODE = 0x03 }; + +/// Oversampling bit fields in the control and measurement register +enum Oversampling { + OVERSAMPLING_NONE = 0x00, + OVERSAMPLING_X2 = 0x01, + OVERSAMPLING_X4 = 0x02, + OVERSAMPLING_X8 = 0x03, + OVERSAMPLING_X16 = 0x04, + OVERSAMPLING_X32 = 0x05 +}; + +/// Infinite Impulse Response (IIR) filter bit field in the configuration register +enum IIRFilter { + IIR_FILTER_OFF = 0x00, + IIR_FILTER_2 = 0x01, + IIR_FILTER_4 = 0x02, + IIR_FILTER_8 = 0x03, + IIR_FILTER_16 = 0x04, + IIR_FILTER_32 = 0x05, + IIR_FILTER_64 = 0x06, + IIR_FILTER_128 = 0x07 +}; + +/// This class implements support for the BMP3XX Temperature+Pressure i2c sensor. +class BMP3XXComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + /// Set the oversampling value for the temperature sensor. Default is 16x. + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + } + /// Set the oversampling value for the pressure sensor. Default is 16x. + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + } + /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. + void set_iir_filter_config(IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } + + /// Soft reset the sensor + uint8_t reset(); + /// Start continuous measurement in NORMAL_MODE + bool start_normal_conversion(); + /// Start a one shot measurement in FORCED_MODE + bool start_forced_conversion(); + /// Stop the conversion and return to SLEEP_MODE + bool stop_conversion(); + /// Set the pressure oversampling: OFF, X1, X2, X4, X8, X16, X32 + bool set_pressure_oversampling(Oversampling pressure_oversampling); + /// Set the temperature oversampling: OFF, X1, X2, X4, X8, X16, X32 + bool set_temperature_oversampling(Oversampling temperature_oversampling); + /// Set the IIR filter setting: OFF, 2, 3, 8, 16, 32 + bool set_iir_filter(IIRFilter iir_filter); + /// Get a temperature measurement + bool get_temperature(float &temperature); + /// Get a pressure measurement + bool get_pressure(float &pressure); + /// Get a temperature and pressure measurement + bool get_measurements(float &temperature, float &pressure); + /// Get a temperature and pressure measurement + bool get_measurement(); + /// Set the barometer mode + bool set_mode(OperationMode mode); + /// Set the BMP388 oversampling register + bool set_oversampling_register(Oversampling pressure_oversampling, Oversampling temperature_oversampling); + /// Checks if a measurement is ready + bool data_ready(); + + protected: + Oversampling temperature_oversampling_{OVERSAMPLING_X16}; + Oversampling pressure_oversampling_{OVERSAMPLING_X16}; + IIRFilter iir_filter_{IIR_FILTER_OFF}; + OperationMode operation_mode_{FORCED_MODE}; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + enum ErrorCode { + NONE = 0, + ERROR_COMMUNICATION_FAILED, + ERROR_WRONG_CHIP_ID, + ERROR_SENSOR_STATUS, + ERROR_SENSOR_RESET, + } error_code_{NONE}; + + struct { // The BMP388 compensation trim parameters (coefficients) + uint16_t param_T1; + uint16_t param_T2; + int8_t param_T3; + int16_t param_P1; + int16_t param_P2; + int8_t param_P3; + int8_t param_P4; + uint16_t param_P5; + uint16_t param_P6; + int8_t param_P7; + int8_t param_P8; + int16_t param_P9; + int8_t param_P10; + int8_t param_P11; + } __attribute__((packed)) compensation_params_; + + struct FloatParams { // The BMP388 float point compensation trim parameters + float param_T1; + float param_T2; + float param_T3; + float param_P1; + float param_P2; + float param_P3; + float param_P4; + float param_P5; + float param_P6; + float param_P7; + float param_P8; + float param_P9; + float param_P10; + float param_P11; + } compensation_float_params_; + + union { // Copy of the BMP388's chip id register + struct { + uint8_t chip_id_nvm : 4; + uint8_t chip_id_fixed : 4; + } bit; + uint8_t reg; + } chip_id_ = {.reg = 0}; + + union { // Copy of the BMP388's event register + struct { + uint8_t por_detected : 1; + } bit; + uint8_t reg; + } event_ = {.reg = 0}; + + union { // Copy of the BMP388's interrupt status register + struct { + uint8_t fwm_int : 1; + uint8_t ffull_int : 1; + uint8_t : 1; + uint8_t drdy : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; + + union { // Copy of the BMP388's power control register + struct { + uint8_t press_en : 1; + uint8_t temp_en : 1; + uint8_t : 2; + uint8_t mode : 2; + } bit; + uint8_t reg; + } pwr_ctrl_ = {.reg = 0}; + + union { // Copy of the BMP388's oversampling register + struct { + uint8_t osr_p : 3; + uint8_t osr_t : 3; + } bit; + uint8_t reg; + } osr_ = {.reg = 0}; + + union { // Copy of the BMP388's output data rate register + struct { + uint8_t odr_sel : 5; + } bit; + uint8_t reg; + } odr_ = {.reg = 0}; + + union { // Copy of the BMP388's configuration register + struct { + uint8_t : 1; + uint8_t iir_filter : 3; + } bit; + uint8_t reg; + } config_ = {.reg = 0}; + + // Bosch temperature compensation function + float bmp388_compensate_temperature_(float uncomp_temp); + // Bosch pressure compensation function + float bmp388_compensate_pressure_(float uncomp_press, float t_lin); +}; + +} // namespace bmp3xx +} // namespace esphome diff --git a/esphome/components/bmp3xx/sensor.py b/esphome/components/bmp3xx/sensor.py new file mode 100644 index 0000000000..736e6df3d8 --- /dev/null +++ b/esphome/components/bmp3xx/sensor.py @@ -0,0 +1,100 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@martgras"] +DEPENDENCIES = ["i2c"] + +bmp3xx_ns = cg.esphome_ns.namespace("bmp3xx") +Oversampling = bmp3xx_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32x": Oversampling.OVERSAMPLING_X32, +} + +IIRFilter = bmp3xx_ns.enum("IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": IIRFilter.IIR_FILTER_OFF, + "2X": IIRFilter.IIR_FILTER_2, + "4X": IIRFilter.IIR_FILTER_4, + "8X": IIRFilter.IIR_FILTER_8, + "16X": IIRFilter.IIR_FILTER_16, + "32X": IIRFilter.IIR_FILTER_32, + "64X": IIRFilter.IIR_FILTER_64, + "128X": IIRFilter.IIR_FILTER_128, +} + +BMP3XXComponent = bmp3xx_ns.class_( + "BMP3XXComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BMP3XXComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="2X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add(var.set_iir_filter_config(config[CONF_IIR_FILTER])) + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling_config(conf[CONF_OVERSAMPLING])) + + if CONF_PRESSURE in config: + conf = config[CONF_PRESSURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(conf[CONF_OVERSAMPLING])) diff --git a/esphome/components/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py new file mode 100644 index 0000000000..f8efdf4b2a --- /dev/null +++ b/esphome/components/cd74hc4067/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +from esphome import pins +import esphome.config_validation as cv +from esphome.const import ( + CONF_DELAY, + CONF_ID, +) + +CODEOWNERS = ["@asoehlke"] +AUTO_LOAD = ["sensor", "voltage_sampler"] + +cd74hc4067_ns = cg.esphome_ns.namespace("cd74hc4067") + +CD74HC4067Component = cd74hc4067_ns.class_( + "CD74HC4067Component", cg.Component, cg.PollingComponent +) + +CONF_PIN_S0 = "pin_s0" +CONF_PIN_S1 = "pin_s1" +CONF_PIN_S2 = "pin_s2" +CONF_PIN_S3 = "pin_s3" + +DEFAULT_DELAY = "2ms" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CD74HC4067Component), + cv.Required(CONF_PIN_S0): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S1): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S2): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S3): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DELAY, default=DEFAULT_DELAY + ): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + pin_s0 = await cg.gpio_pin_expression(config[CONF_PIN_S0]) + cg.add(var.set_pin_s0(pin_s0)) + pin_s1 = await cg.gpio_pin_expression(config[CONF_PIN_S1]) + cg.add(var.set_pin_s1(pin_s1)) + pin_s2 = await cg.gpio_pin_expression(config[CONF_PIN_S2]) + cg.add(var.set_pin_s2(pin_s2)) + pin_s3 = await cg.gpio_pin_expression(config[CONF_PIN_S3]) + cg.add(var.set_pin_s3(pin_s3)) + + cg.add(var.set_switch_delay(config[CONF_DELAY])) diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp new file mode 100644 index 0000000000..ea789c2d8c --- /dev/null +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -0,0 +1,86 @@ +#include "cd74hc4067.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cd74hc4067 { + +static const char *const TAG = "cd74hc4067"; + +float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } + +void CD74HC4067Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up CD74HC4067..."); + + this->pin_s0_->setup(); + this->pin_s1_->setup(); + this->pin_s2_->setup(); + this->pin_s3_->setup(); + + // set other pin, so that activate_pin will really switch + this->active_pin_ = 1; + this->activate_pin(0); +} + +void CD74HC4067Component::dump_config() { + ESP_LOGCONFIG(TAG, "CD74HC4067 Multiplexer:"); + LOG_PIN(" S0 Pin: ", this->pin_s0_); + LOG_PIN(" S1 Pin: ", this->pin_s1_); + LOG_PIN(" S2 Pin: ", this->pin_s2_); + LOG_PIN(" S3 Pin: ", this->pin_s3_); + ESP_LOGCONFIG(TAG, "switch delay: %d", this->switch_delay_); +} + +void CD74HC4067Component::activate_pin(uint8_t pin) { + if (this->active_pin_ != pin) { + ESP_LOGD(TAG, "switch to input %d", pin); + + static int mux_channel[16][4] = { + {0, 0, 0, 0}, // channel 0 + {1, 0, 0, 0}, // channel 1 + {0, 1, 0, 0}, // channel 2 + {1, 1, 0, 0}, // channel 3 + {0, 0, 1, 0}, // channel 4 + {1, 0, 1, 0}, // channel 5 + {0, 1, 1, 0}, // channel 6 + {1, 1, 1, 0}, // channel 7 + {0, 0, 0, 1}, // channel 8 + {1, 0, 0, 1}, // channel 9 + {0, 1, 0, 1}, // channel 10 + {1, 1, 0, 1}, // channel 11 + {0, 0, 1, 1}, // channel 12 + {1, 0, 1, 1}, // channel 13 + {0, 1, 1, 1}, // channel 14 + {1, 1, 1, 1} // channel 15 + }; + this->pin_s0_->digital_write(mux_channel[pin][0]); + this->pin_s1_->digital_write(mux_channel[pin][1]); + this->pin_s2_->digital_write(mux_channel[pin][2]); + this->pin_s3_->digital_write(mux_channel[pin][3]); + // small delay is needed to let the multiplexer switch + delay(this->switch_delay_); + this->active_pin_ = pin; + } +} + +CD74HC4067Sensor::CD74HC4067Sensor(CD74HC4067Component *parent) : parent_(parent) {} + +void CD74HC4067Sensor::update() { + float value_v = this->sample(); + this->publish_state(value_v); +} + +float CD74HC4067Sensor::get_setup_priority() const { return this->parent_->get_setup_priority() - 1.0f; } + +float CD74HC4067Sensor::sample() { + this->parent_->activate_pin(this->pin_); + return this->source_->sample(); +} + +void CD74HC4067Sensor::dump_config() { + LOG_SENSOR(TAG, "CD74HC4067 Sensor", this); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + LOG_UPDATE_INTERVAL(this); +} + +} // namespace cd74hc4067 +} // namespace esphome diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h new file mode 100644 index 0000000000..4a5c2e4e35 --- /dev/null +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -0,0 +1,65 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +namespace esphome { +namespace cd74hc4067 { + +class CD74HC4067Component : public Component { + public: + /// Set up the internal sensor array. + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + /// setting pin active by setting the right combination of the four multiplexer input pins + void activate_pin(uint8_t pin); + + /// set the pin connected to multiplexer control pin 0 + void set_pin_s0(InternalGPIOPin *pin) { this->pin_s0_ = pin; } + /// set the pin connected to multiplexer control pin 1 + void set_pin_s1(InternalGPIOPin *pin) { this->pin_s1_ = pin; } + /// set the pin connected to multiplexer control pin 2 + void set_pin_s2(InternalGPIOPin *pin) { this->pin_s2_ = pin; } + /// set the pin connected to multiplexer control pin 3 + void set_pin_s3(InternalGPIOPin *pin) { this->pin_s3_ = pin; } + + /// set the delay needed after an input switch + void set_switch_delay(uint32_t switch_delay) { this->switch_delay_ = switch_delay; } + + private: + InternalGPIOPin *pin_s0_; + InternalGPIOPin *pin_s1_; + InternalGPIOPin *pin_s2_; + InternalGPIOPin *pin_s3_; + /// the currently active pin + uint8_t active_pin_; + uint32_t switch_delay_; +}; + +class CD74HC4067Sensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { + public: + CD74HC4067Sensor(CD74HC4067Component *parent); + + void update() override; + + void dump_config() override; + /// `HARDWARE_LATE` setup priority. + float get_setup_priority() const override; + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_source(voltage_sampler::VoltageSampler *source) { this->source_ = source; } + + float sample() override; + + protected: + CD74HC4067Component *parent_; + /// The sampling source to read values from. + voltage_sampler::VoltageSampler *source_; + + uint8_t pin_; +}; +} // namespace cd74hc4067 +} // namespace esphome diff --git a/esphome/components/cd74hc4067/sensor.py b/esphome/components/cd74hc4067/sensor.py new file mode 100644 index 0000000000..7c7cf9ccb7 --- /dev/null +++ b/esphome/components/cd74hc4067/sensor.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_ID, + CONF_SENSOR, + CONF_NUMBER, + ICON_FLASH, + UNIT_VOLT, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_VOLTAGE, +) +from . import cd74hc4067_ns, CD74HC4067Component + +DEPENDENCIES = ["cd74hc4067"] + +CD74HC4067Sensor = cd74hc4067_ns.class_( + "CD74HC4067Sensor", + sensor.Sensor, + cg.PollingComponent, + voltage_sampler.VoltageSampler, +) + +CONF_CD74HC4067_ID = "cd74hc4067_id" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_FLASH, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(CD74HC4067Sensor), + cv.GenerateID(CONF_CD74HC4067_ID): cv.use_id(CD74HC4067Component), + cv.Required(CONF_NUMBER): cv.int_range(0, 15), + cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_CD74HC4067_ID]) + + var = cg.new_Pvariable(config[CONF_ID], parent) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + cg.add(var.set_pin(config[CONF_NUMBER])) + + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_source(sens)) diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index b47d9b0141..76adfb42bb 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -10,21 +10,22 @@ climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); - if (supports_cool_) + if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); - if (supports_heat_) + if (this->supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - if (supports_dry_) + if (this->supports_dry_) traits.add_supported_mode(climate::CLIMATE_MODE_DRY); - if (supports_fan_only_) + if (this->supports_fan_only_) traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); traits.set_supports_two_point_target_temperature(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); - traits.set_supported_fan_modes(fan_modes_); - traits.set_supported_swing_modes(swing_modes_); + traits.set_supported_fan_modes(this->fan_modes_); + traits.set_supported_swing_modes(this->swing_modes_); + traits.set_supported_presets(this->presets_); return traits; } @@ -50,6 +51,7 @@ void ClimateIR::setup() { roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); this->fan_mode = climate::CLIMATE_FAN_AUTO; this->swing_mode = climate::CLIMATE_SWING_OFF; + this->preset = climate::CLIMATE_PRESET_NONE; } // Never send nan to HA if (std::isnan(this->target_temperature)) @@ -65,6 +67,8 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->fan_mode = *call.get_fan_mode(); if (call.get_swing_mode().has_value()) this->swing_mode = *call.get_swing_mode(); + if (call.get_preset().has_value()) + this->preset = *call.get_preset(); this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 677021da29..5be4fc06f5 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -22,7 +22,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}) { + std::set swing_modes = {}, std::set presets = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; @@ -30,6 +30,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: this->supports_fan_only_ = supports_fan_only; this->fan_modes_ = std::move(fan_modes); this->swing_modes_ = std::move(swing_modes); + this->presets_ = std::move(presets); } void setup() override; @@ -61,6 +62,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_fan_only_{false}; std::set fan_modes_ = {}; std::set swing_modes_ = {}; + std::set presets_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index c9145e4ecf..76ec1627c2 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -1,4 +1,5 @@ #include "coolix.h" +#include "esphome/components/remote_base/coolix_protocol.h" #include "esphome/core/log.h" namespace esphome { @@ -6,29 +7,29 @@ namespace coolix { static const char *const TAG = "coolix.climate"; -const uint32_t COOLIX_OFF = 0xB27BE0; -const uint32_t COOLIX_SWING = 0xB26BE0; -const uint32_t COOLIX_LED = 0xB5F5A5; -const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; +static const uint32_t COOLIX_OFF = 0xB27BE0; +static const uint32_t COOLIX_SWING = 0xB26BE0; +static const uint32_t COOLIX_LED = 0xB5F5A5; +static const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -const uint8_t COOLIX_COOL = 0b0000; -const uint8_t COOLIX_DRY_FAN = 0b0100; -const uint8_t COOLIX_AUTO = 0b1000; -const uint8_t COOLIX_HEAT = 0b1100; -const uint32_t COOLIX_MODE_MASK = 0b1100; -const uint32_t COOLIX_FAN_MASK = 0xF000; -const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; -const uint32_t COOLIX_FAN_AUTO = 0xB000; -const uint32_t COOLIX_FAN_MIN = 0x9000; -const uint32_t COOLIX_FAN_MED = 0x5000; -const uint32_t COOLIX_FAN_MAX = 0x3000; +static const uint8_t COOLIX_COOL = 0b0000; +static const uint8_t COOLIX_DRY_FAN = 0b0100; +static const uint8_t COOLIX_AUTO = 0b1000; +static const uint8_t COOLIX_HEAT = 0b1100; +static const uint32_t COOLIX_MODE_MASK = 0b1100; +static const uint32_t COOLIX_FAN_MASK = 0xF000; +static const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; +static const uint32_t COOLIX_FAN_AUTO = 0xB000; +static const uint32_t COOLIX_FAN_MIN = 0x9000; +static const uint32_t COOLIX_FAN_MED = 0x5000; +static const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature -const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; -const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. -const uint32_t COOLIX_TEMP_MASK = 0b11110000; -const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { +static const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; +static const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. +static const uint32_t COOLIX_TEMP_MASK = 0b11110000; +static const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { 0b00000000, // 17C 0b00010000, // 18c 0b00110000, // 19C @@ -45,17 +46,6 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { 0b10110000 // 30C }; -// Constants -static const uint32_t BIT_MARK_US = 660; -static const uint32_t HEADER_MARK_US = 560 * 8; -static const uint32_t HEADER_SPACE_US = 560 * 8; -static const uint32_t BIT_ONE_SPACE_US = 1500; -static const uint32_t BIT_ZERO_SPACE_US = 450; -static const uint32_t FOOTER_MARK_US = BIT_MARK_US; -static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; - -const uint16_t COOLIX_BITS = 24; - void CoolixClimate::transmit_state() { uint32_t remote_state = 0xB20F00; @@ -111,119 +101,60 @@ void CoolixClimate::transmit_state() { } } } - ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); + ESP_LOGV(TAG, "Sending coolix code: 0x%06X", remote_state); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); - - data->set_carrier_frequency(38000); - uint16_t repeat = 1; - for (uint16_t r = 0; r <= repeat; r++) { - // Header - data->mark(HEADER_MARK_US); - data->space(HEADER_SPACE_US); - // Data - // Break data into bytes, starting at the Most Significant - // Byte. Each byte then being sent normal, then followed inverted. - for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) { - // Grab a bytes worth of data. - uint8_t byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF; - // Normal - for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(BIT_MARK_US); - data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); - } - // Inverted - for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(BIT_MARK_US); - data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); - } - } - // Footer - data->mark(BIT_MARK_US); - data->space(FOOTER_SPACE_US); // Pause before repeating - } - + remote_base::CoolixProtocol().encode(data, remote_state); transmit.perform(); } -bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { +bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { + auto decoded = remote_base::CoolixProtocol().decode(data); + if (!decoded.has_value()) + return false; // Decoded remote state y 3 bytes long code. - uint32_t remote_state = 0; - // The protocol sends the data twice, read here - uint32_t loop_read; - for (uint16_t loop = 1; loop <= 2; loop++) { - if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) - return false; - loop_read = 0; - for (uint8_t a_byte = 0; a_byte < 3; a_byte++) { - uint8_t byte = 0; - for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { - if (data.expect_item(BIT_MARK_US, BIT_ONE_SPACE_US)) - byte |= 1 << a_bit; - else if (!data.expect_item(BIT_MARK_US, BIT_ZERO_SPACE_US)) - return false; - } - // Need to see this segment inverted - for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { - bool bit = byte & (1 << a_bit); - if (!data.expect_item(BIT_MARK_US, bit ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) - return false; - } - // Receiving MSB first: reorder bytes - loop_read |= byte << ((2 - a_byte) * 8); - } - // Footer Mark - if (!data.expect_mark(BIT_MARK_US)) - return false; - if (loop == 1) { - // Back up state on first loop - remote_state = loop_read; - if (!data.expect_space(FOOTER_SPACE_US)) - return false; - } - } - - ESP_LOGV(TAG, "Decoded 0x%02X", remote_state); - if (remote_state != loop_read || (remote_state & 0xFF0000) != 0xB20000) + uint32_t remote_state = *decoded; + ESP_LOGV(TAG, "Decoded 0x%06X", remote_state); + if ((remote_state & 0xFF0000) != 0xB20000) return false; if (remote_state == COOLIX_OFF) { - this->mode = climate::CLIMATE_MODE_OFF; + parent->mode = climate::CLIMATE_MODE_OFF; } else if (remote_state == COOLIX_SWING) { - this->swing_mode = - this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + parent->swing_mode = + parent->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) - this->mode = climate::CLIMATE_MODE_HEAT; + parent->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + parent->mode = climate::CLIMATE_MODE_HEAT_COOL; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) - this->mode = climate::CLIMATE_MODE_DRY; + parent->mode = climate::CLIMATE_MODE_DRY; else - this->mode = climate::CLIMATE_MODE_FAN_ONLY; + parent->mode = climate::CLIMATE_MODE_FAN_ONLY; } else - this->mode = climate::CLIMATE_MODE_COOL; + parent->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_HEAT_COOL || - this->mode == climate::CLIMATE_MODE_DRY) - this->fan_mode = climate::CLIMATE_FAN_AUTO; + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL || + parent->mode == climate::CLIMATE_MODE_DRY) + parent->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) - this->fan_mode = climate::CLIMATE_FAN_LOW; + parent->fan_mode = climate::CLIMATE_FAN_LOW; else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) - this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + parent->fan_mode = climate::CLIMATE_FAN_MEDIUM; else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) - this->fan_mode = climate::CLIMATE_FAN_HIGH; + parent->fan_mode = climate::CLIMATE_FAN_HIGH; // Temperature uint8_t temperature_code = remote_state & COOLIX_TEMP_MASK; for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) if (COOLIX_TEMP_MAP[i] == temperature_code) - this->target_temperature = i + COOLIX_TEMP_MIN; + parent->target_temperature = i + COOLIX_TEMP_MIN; } - this->publish_state(); + parent->publish_state(); return true; } diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index caf93f7621..3419795875 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -26,11 +26,15 @@ class CoolixClimate : public climate_ir::ClimateIR { climate_ir::ClimateIR::control(call); } + /// This static method can be used in other climate components that accept the Coolix protocol. See midea_ir for + /// example. + static bool on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data); + protected: /// Transmit via IR the state of this climate controller. void transmit_state() override; /// Handle received IR Buffer - bool on_receive(remote_base::RemoteReceiveData data) override; + bool on_receive(remote_base::RemoteReceiveData data) override { return CoolixClimate::on_coolix(this, data); } bool send_swing_cmd_{false}; }; diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index a8d3d691a4..21f35f14de 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -210,7 +210,10 @@ Cover::Cover() : Cover("") {} std::string Cover::get_device_class() { if (this->device_class_override_.has_value()) return *this->device_class_override_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->device_class(); +#pragma GCC diagnostic pop } bool Cover::is_fully_open() const { return this->position == COVER_OPEN; } bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index a67f8d2393..779e4a2a46 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -169,6 +169,7 @@ class Cover : public EntityBase { friend CoverCall; virtual void control(const CoverCall &call) = 0; + ESPDEPRECATED("device_class() is deprecated, set property during config validation instead.", "2022.01") virtual std::string device_class(); optional restore_state_(); diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 8d7f2e4deb..3610e79447 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -38,10 +38,9 @@ void DallasComponent::setup() { raw_sensors = this->one_wire_->search_vec(); for (auto &address : raw_sensors) { - std::string s = uint64_to_string(address); auto *address8 = reinterpret_cast(&address); if (crc8(address8, 7) != address8[7]) { - ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", s.c_str()); + ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str()); continue; } if (address8[0] != DALLAS_MODEL_DS18S20 && address8[0] != DALLAS_MODEL_DS1822 && @@ -77,8 +76,7 @@ void DallasComponent::dump_config() { } else { ESP_LOGD(TAG, " Found sensors:"); for (auto &address : this->found_sensors_) { - std::string s = uint64_to_string(address); - ESP_LOGD(TAG, " 0x%s", s.c_str()); + ESP_LOGD(TAG, " 0x%s", format_hex(address).c_str()); } } @@ -147,7 +145,7 @@ void DallasTemperatureSensor::set_index(uint8_t index) { this->index_ = index; } uint8_t *DallasTemperatureSensor::get_address8() { return reinterpret_cast(&this->address_); } const std::string &DallasTemperatureSensor::get_address_name() { if (this->address_name_.empty()) { - this->address_name_ = std::string("0x") + uint64_to_string(this->address_); + this->address_name_ = std::string("0x") + format_hex(this->address_); } return this->address_name_; @@ -237,7 +235,7 @@ float DallasTemperatureSensor::get_temp_c() { return temp / 128.0f; } -std::string DallasTemperatureSensor::unique_id() { return "dallas-" + uint64_to_string(this->address_); } +std::string DallasTemperatureSensor::unique_id() { return "dallas-" + str_upper_case(format_hex(this->address_)); } } // namespace dallas } // namespace esphome diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 4c47c32ccc..6194a55205 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path import esphome.codegen as cg import esphome.config_validation as cv from esphome.components.packages import validate_source_shorthand +from esphome.wizard import wizard_file from esphome.yaml_util import dump @@ -48,12 +49,24 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N if p.exists(): raise FileExistsError - config = { - "substitutions": {"name": name}, - "packages": {project_name: import_url}, - "esphome": {"name_add_mac_suffix": False}, - } - p.write_text( - dump(config) + WIFI_CONFIG, - encoding="utf8", - ) + if project_name == "esphome.web": + p.write_text( + wizard_file( + name=name, + platform="ESP32" if "esp32" in import_url else "ESP8266", + board="esp32dev" if "esp32" in import_url else "esp01_1m", + ssid="!secret wifi_ssid", + psk="!secret wifi_password", + ), + encoding="utf8", + ) + else: + config = { + "substitutions": {"name": name}, + "packages": {project_name: import_url}, + "esphome": {"name_add_mac_suffix": False}, + } + p.write_text( + dump(config) + WIFI_CONFIG, + encoding="utf8", + ) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 40eb20fa6e..f3d0bded13 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -101,7 +101,7 @@ void DebugComponent::dump_config() { info.features &= ~CHIP_FEATURE_BT; } if (info.features) - features += "Other:" + uint64_to_string(info.features); + features += "Other:" + format_hex(info.features); ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores, info.revision); diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index f47888b8eb..ba4c2c0d7e 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -41,15 +41,30 @@ EXT1_WAKEUP_MODES = { "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, } +WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration") CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode" CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup" CONF_TOUCH_WAKEUP = "touch_wakeup" +CONF_DEFAULT = "default" +CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason" +CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason" + +WAKEUP_CAUSES_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEFAULT): cv.positive_time_period_milliseconds, + cv.Optional(CONF_TOUCH_WAKEUP_REASON): cv.positive_time_period_milliseconds, + cv.Optional(CONF_GPIO_WAKEUP_REASON): cv.positive_time_period_milliseconds, + } +) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(DeepSleepComponent), - cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_RUN_DURATION): cv.Any( + cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA), + cv.positive_time_period_milliseconds, + ), cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_WAKEUP_PIN): cv.All( cv.only_on_esp32, pins.internal_gpio_input_pin_schema, validate_pin_number @@ -85,7 +100,28 @@ async def to_code(config): if CONF_WAKEUP_PIN_MODE in config: cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE])) if CONF_RUN_DURATION in config: - cg.add(var.set_run_duration(config[CONF_RUN_DURATION])) + run_duration_config = config[CONF_RUN_DURATION] + if not isinstance(run_duration_config, dict): + cg.add(var.set_run_duration(config[CONF_RUN_DURATION])) + else: + default_run_duration = run_duration_config[CONF_DEFAULT] + wakeup_cause_to_run_duration = cg.StructInitializer( + WakeupCauseToRunDuration, + ("default_cause", default_run_duration), + ( + "touch_cause", + run_duration_config.get( + CONF_TOUCH_WAKEUP_REASON, default_run_duration + ), + ), + ( + "gpio_cause", + run_duration_config.get( + CONF_GPIO_WAKEUP_REASON, default_run_duration + ), + ), + ) + cg.add(var.set_run_duration(wakeup_cause_to_run_duration)) if CONF_ESP32_EXT1_WAKEUP in config: conf = config[CONF_ESP32_EXT1_WAKEUP] diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index c854b6da6e..7774014d3d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -13,12 +13,35 @@ static const char *const TAG = "deep_sleep"; bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +optional DeepSleepComponent::get_run_duration_() const { +#ifdef USE_ESP32 + if (this->wakeup_cause_to_run_duration_.has_value()) { + esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); + switch (wakeup_cause) { + case ESP_SLEEP_WAKEUP_EXT0: + case ESP_SLEEP_WAKEUP_EXT1: + return this->wakeup_cause_to_run_duration_->gpio_cause; + case ESP_SLEEP_WAKEUP_TOUCHPAD: + return this->wakeup_cause_to_run_duration_->touch_cause; + default: + return this->wakeup_cause_to_run_duration_->default_cause; + } + } +#endif + return this->run_duration_; +} + void DeepSleepComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Deep Sleep..."); global_has_deep_sleep = true; - if (this->run_duration_.has_value()) - this->set_timeout(*this->run_duration_, [this]() { this->begin_sleep(); }); + const optional run_duration = get_run_duration_(); + if (run_duration.has_value()) { + ESP_LOGI(TAG, "Scheduling Deep Sleep to start in %u ms", *run_duration); + this->set_timeout(*run_duration, [this]() { this->begin_sleep(); }); + } else { + ESP_LOGD(TAG, "Not scheduling Deep Sleep, as no run duration is configured."); + } } void DeepSleepComponent::dump_config() { ESP_LOGCONFIG(TAG, "Setting up Deep Sleep..."); @@ -33,6 +56,11 @@ void DeepSleepComponent::dump_config() { if (wakeup_pin_ != nullptr) { LOG_PIN(" Wakeup Pin: ", this->wakeup_pin_); } + if (this->wakeup_cause_to_run_duration_.has_value()) { + ESP_LOGCONFIG(TAG, " Default Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->default_cause); + ESP_LOGCONFIG(TAG, " Touch Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->touch_cause); + ESP_LOGCONFIG(TAG, " GPIO Wakeup Run Duration: %u ms", this->wakeup_cause_to_run_duration_->gpio_cause); + } #endif } void DeepSleepComponent::loop() { @@ -49,6 +77,9 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { } void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } +void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { + wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; +} #endif void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index d7969ba999..59df199a9f 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -32,6 +32,15 @@ struct Ext1Wakeup { esp_sleep_ext1_wakeup_mode_t wakeup_mode; }; +struct WakeupCauseToRunDuration { + // Run duration if woken up by timer or any other reason besides those below. + uint32_t default_cause; + // Run duration if woken up by touch pads. + uint32_t touch_cause; + // Run duration if woken up by GPIO pins. + uint32_t gpio_cause; +}; + #endif template class EnterDeepSleepAction; @@ -59,6 +68,11 @@ class DeepSleepComponent : public Component { void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); void set_touch_wakeup(bool touch_wakeup); + + // Set the duration in ms for how long the code should run before entering + // deep sleep mode, according to the cause the ESP32 has woken. + void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); + #endif /// Set a duration in ms for how long the code should run before entering deep sleep mode. void set_run_duration(uint32_t time_ms); @@ -75,12 +89,17 @@ class DeepSleepComponent : public Component { void prevent_deep_sleep(); protected: + // Returns nullopt if no run duration is set. Otherwise, returns the run + // duration before entering deep sleep. + optional get_run_duration_() const; + optional sleep_duration_; #ifdef USE_ESP32 InternalGPIOPin *wakeup_pin_; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; optional ext1_wakeup_; optional touch_wakeup_; + optional wakeup_cause_to_run_duration_; #endif optional run_duration_; bool next_enter_deep_sleep_{false}; diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 1458629acd..b97fb4ae23 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -5,6 +5,7 @@ #include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" namespace esphome { namespace display { @@ -15,7 +16,8 @@ const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); void DisplayBuffer::init_internal_(uint32_t buffer_length) { - this->buffer_ = new (std::nothrow) uint8_t[buffer_length]; // NOLINT + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_ = allocator.allocate(buffer_length); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); return; diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index aed22312a7..9a881c81f0 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -23,22 +23,24 @@ void DutyCycleSensor::dump_config() { } void DutyCycleSensor::update() { const uint32_t now = micros(); + const uint32_t last_interrupt = this->store_.last_interrupt; // Read the measurement taken by the interrupt + uint32_t on_time = this->store_.on_time; + + this->store_.on_time = 0; // Start new measurement, exactly aligned with the micros() reading + this->store_.last_interrupt = now; + if (this->last_update_ != 0) { const bool level = this->store_.last_level; - const uint32_t last_interrupt = this->store_.last_interrupt; - uint32_t on_time = this->store_.on_time; if (level) on_time += now - last_interrupt; const float total_time = float(now - this->last_update_); - const float value = (on_time / total_time) * 100.0f; + const float value = (on_time * 100.0f) / total_time; ESP_LOGD(TAG, "'%s' Got duty cycle=%.1f%%", this->get_name().c_str(), value); this->publish_state(value); } - this->store_.on_time = 0; - this->store_.last_interrupt = now; this->last_update_ = now; } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d6f1180aa7..161803eaf4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2,8 +2,9 @@ from dataclasses import dataclass from typing import Union from pathlib import Path import logging +import os -from esphome.helpers import write_file_if_changed +from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, @@ -295,6 +296,8 @@ async def to_code(config): conf = config[CONF_FRAMEWORK] cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) + cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_platformio_option("framework", "espidf") cg.add_build_flag("-DUSE_ESP_IDF") @@ -412,3 +415,10 @@ def copy_files(): CORE.relative_build_path("partitions.csv"), IDF_PARTITIONS_CSV, ) + + dir = os.path.dirname(__file__) + post_build_file = os.path.join(dir, "post_build.py") + copy_file_if_changed( + post_build_file, + CORE.relative_build_path("post_build.py"), + ) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 7f7bb2259f..56fd4932b4 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -261,6 +261,37 @@ ESP32_BOARD_PINS = { "SS": 33, "TX": 17, }, + "featheresp32-s2": { + "SDA": 3, + "SCL": 4, + "SS": 42, + "MOSI": 35, + "SCK": 36, + "MISO": 37, + "A0": 18, + "A1": 17, + "A10": 27, + "A11": 12, + "A12": 13, + "A13": 35, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "LED": 13, + "TX": 39, + "RX": 38, + "T5": 5, + "T8": 8, + "T9": 9, + "T10": 10, + "T11": 11, + "T12": 12, + "T13": 13, + "T14": 14, + "DAC1": 17, + "DAC2": 18, + }, "firebeetle32": {"LED": 2}, "fm-devkit": { "D0": 34, diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index 425d77b343..dbafb73dba 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -18,7 +18,7 @@ _ESP_SDIO_PINS = { 11: "Flash Command", } -_ESP32_STRAPPING_PINS = {0, 2, 4, 15} +_ESP32_STRAPPING_PINS = {0, 2, 4, 12, 15} _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/esp32/post_build.py b/esphome/components/esp32/post_build.py new file mode 100644 index 0000000000..7feaf9e8e5 --- /dev/null +++ b/esphome/components/esp32/post_build.py @@ -0,0 +1,43 @@ +# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 + +import esptool + +# pylint: disable=E0602 +Import("env") # noqa + + +def esp32_create_combined_bin(source, target, env): + print("Generating combined binary for serial flashing") + app_offset = 0x10000 + + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") + sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + chip = env.get("BOARD_MCU") + flash_size = env.BoardConfig().get("upload.flash_size") + cmd = [ + "--chip", + chip, + "merge_bin", + "-o", + new_file_name, + "--flash_size", + flash_size, + ] + print(" Offset | File") + for section in sections: + sect_adr, sect_file = section.split(" ", 1) + print(f" - {sect_adr} | {sect_file}") + cmd += [sect_adr, sect_file] + + print(f" - {hex(app_offset)} | {firmware_name}") + cmd += [hex(app_offset), firmware_name] + + print() + print(f"Using esptool.py arguments: {' '.join(cmd)}") + print() + esptool.main(cmd) + + +# pylint: disable=E0602 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 303cb34aa7..084dab4c84 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -483,6 +483,7 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData } void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + this->scan_result_ = param; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) this->address_[i] = param.bda[i]; this->address_type_ = param.ble_addr_type; @@ -524,7 +525,7 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } for (auto &data : this->manufacturer_datas_) { - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str()); + ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str()); if (this->get_ibeacon().has_value()) { auto ibeacon = this->get_ibeacon().value(); ESP_LOGVV(TAG, " iBeacon data:"); @@ -537,10 +538,10 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e for (auto &data : this->service_datas_) { ESP_LOGVV(TAG, " Service data:"); ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); - ESP_LOGVV(TAG, " Data: %s", hexencode(data.data).c_str()); + ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); } - ESP_LOGVV(TAG, "Adv data: %s", hexencode(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); + ESP_LOGVV(TAG, "Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 02e102f06c..9ff2a5a861 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -97,6 +97,8 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } + const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } + optional get_ibeacon() const { for (auto &it : this->manufacturer_datas_) { auto res = ESPBLEiBeacon::from_manufacturer_data(it); @@ -121,6 +123,7 @@ class ESPBTDevice { std::vector service_uuids_; std::vector manufacturer_datas_{}; std::vector service_datas_{}; + esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; }; class ESP32BLETracker; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 2b1890267f..d42d4f5de3 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -19,6 +19,8 @@ from esphome.cpp_helpers import setup_entity DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["psram"] + esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") @@ -57,6 +59,9 @@ CONF_IDLE_FRAMERATE = "idle_framerate" CONF_JPEG_QUALITY = "jpeg_quality" CONF_VERTICAL_FLIP = "vertical_flip" CONF_HORIZONTAL_MIRROR = "horizontal_mirror" +CONF_AEC2 = "aec2" +CONF_AE_LEVEL = "ae_level" +CONF_AEC_VALUE = "aec_value" CONF_SATURATION = "saturation" CONF_TEST_PATTERN = "test_pattern" @@ -102,6 +107,9 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_SATURATION, default=0): camera_range_param, cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, + cv.Optional(CONF_AEC2, default=False): cv.boolean, + cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, + cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -116,6 +124,9 @@ SETTERS = { CONF_JPEG_QUALITY: "set_jpeg_quality", CONF_VERTICAL_FLIP: "set_vertical_flip", CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", + CONF_AEC2: "set_aec2", + CONF_AE_LEVEL: "set_ae_level", + CONF_AEC_VALUE: "set_aec_value", CONF_CONTRAST: "set_contrast", CONF_BRIGHTNESS: "set_brightness", CONF_SATURATION: "set_saturation", @@ -144,9 +155,7 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_ESP32_CAMERA") - cg.add_build_flag("-DBOARD_HAS_PSRAM") if CORE.using_esp_idf: cg.add_library("espressif/esp32-camera", "1.0.0") add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) - add_idf_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 6f93532f47..7d11f98d09 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -26,6 +26,9 @@ void ESP32Camera::setup() { sensor_t *s = esp_camera_sensor_get(); s->set_vflip(s, this->vertical_flip_); s->set_hmirror(s, this->horizontal_mirror_); + s->set_aec2(s, this->aec2_); // 0 = disable , 1 = enable + s->set_ae_level(s, this->ae_level_); // -2 to 2 + s->set_aec_value(s, this->aec_value_); // 0 to 1200 s->set_contrast(s, this->contrast_); s->set_brightness(s, this->brightness_); s->set_saturation(s, this->saturation_); @@ -46,9 +49,6 @@ void ESP32Camera::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 Camera:"); ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_)); -#ifdef USE_ARDUINO - ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound())); -#endif // USE_ARDUINO ESP_LOGCONFIG(TAG, " Data Pins: D0:%d D1:%d D2:%d D3:%d D4:%d D5:%d D6:%d D7:%d", conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7); ESP_LOGCONFIG(TAG, " VSYNC Pin: %d", conf.pin_vsync); @@ -111,9 +111,9 @@ void ESP32Camera::dump_config() { // ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb); // ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain); // ESP_LOGCONFIG(TAG, " Auto Exposure Control: %u", st.aec); - // ESP_LOGCONFIG(TAG, " Auto Exposure Control 2: %u", st.aec2); - // ESP_LOGCONFIG(TAG, " Auto Exposure Level: %d", st.ae_level); - // ESP_LOGCONFIG(TAG, " Auto Exposure Value: %u", st.aec_value); + ESP_LOGCONFIG(TAG, " Auto Exposure Control 2: %u", st.aec2); + ESP_LOGCONFIG(TAG, " Auto Exposure Level: %d", st.ae_level); + ESP_LOGCONFIG(TAG, " Auto Exposure Value: %u", st.aec_value); // ESP_LOGCONFIG(TAG, " AGC: %u", st.agc); // ESP_LOGCONFIG(TAG, " AGC Gain: %u", st.agc_gain); // ESP_LOGCONFIG(TAG, " Gain Ceiling: %u", st.gainceiling); @@ -133,6 +133,13 @@ void ESP32Camera::loop() { this->current_image_.reset(); } + // request idle image every idle_update_interval + const uint32_t now = millis(); + if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { + this->last_idle_request_ = now; + this->request_image(IDLE); + } + // Check if we should fetch a new image if (!this->has_requested_image_()) return; @@ -140,7 +147,6 @@ void ESP32Camera::loop() { // image is still in use return; } - const uint32_t now = millis(); if (now - this->last_update_ <= this->max_update_interval_) return; @@ -157,12 +163,12 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } - this->current_image_ = std::make_shared(fb); + this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); ESP_LOGD(TAG, "Got Image: len=%u", fb->len); this->new_image_callback_.call(this->current_image_); this->last_update_ = now; - this->single_requester_ = false; + this->single_requesters_ = 0; } void ESP32Camera::framebuffer_task(void *pv) { while (true) { @@ -250,29 +256,18 @@ void ESP32Camera::add_image_callback(std::functionvertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } +void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; } +void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; } +void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; } void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; } void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; } void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; } float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } uint32_t ESP32Camera::hash_base() { return 3010542557UL; } -void ESP32Camera::request_image() { this->single_requester_ = true; } -void ESP32Camera::request_stream() { this->last_stream_request_ = millis(); } -bool ESP32Camera::has_requested_image_() const { - if (this->single_requester_) - // single request - return true; - - uint32_t now = millis(); - if (now - this->last_stream_request_ < 5000) - // stream request - return true; - - if (this->idle_update_interval_ != 0 && now - this->last_update_ > this->idle_update_interval_) - // idle update - return true; - - return false; -} +void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= 1 << requester; } +void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= 1 << requester; } +void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1 << requester); } +bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; } bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { this->max_update_interval_ = max_update_interval; @@ -301,7 +296,10 @@ uint8_t *CameraImageReader::peek_data_buffer() { return this->image_->get_data_b camera_fb_t *CameraImage::get_raw_buffer() { return this->buffer_; } uint8_t *CameraImage::get_data_buffer() { return this->buffer_->buf; } size_t CameraImage::get_data_length() { return this->buffer_->len; } -CameraImage::CameraImage(camera_fb_t *buffer) : buffer_(buffer) {} +bool CameraImage::was_requested_by(CameraRequester requester) const { + return (this->requesters_ & (1 << requester)) != 0; +} +CameraImage::CameraImage(camera_fb_t *buffer, uint8_t requesters) : buffer_(buffer), requesters_(requesters) {} } // namespace esp32_camera } // namespace esphome diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 84f8d9cbea..b2670078f3 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -14,15 +14,19 @@ namespace esp32_camera { class ESP32Camera; +enum CameraRequester { IDLE, API_REQUESTER, WEB_REQUESTER }; + class CameraImage { public: - CameraImage(camera_fb_t *buffer); + CameraImage(camera_fb_t *buffer, uint8_t requester); camera_fb_t *get_raw_buffer(); uint8_t *get_data_buffer(); size_t get_data_length(); + bool was_requested_by(CameraRequester requester) const; protected: camera_fb_t *buffer_; + uint8_t requesters_; }; class CameraImageReader { @@ -67,6 +71,9 @@ class ESP32Camera : public Component, public EntityBase { void set_power_down_pin(uint8_t pin); void set_vertical_flip(bool vertical_flip); void set_horizontal_mirror(bool horizontal_mirror); + void set_aec2(bool aec2); + void set_ae_level(int ae_level); + void set_aec_value(uint32_t aec_value); void set_contrast(int contrast); void set_brightness(int brightness); void set_saturation(int saturation); @@ -78,8 +85,9 @@ class ESP32Camera : public Component, public EntityBase { void dump_config() override; void add_image_callback(std::function)> &&f); float get_setup_priority() const override; - void request_stream(); - void request_image(); + void start_stream(CameraRequester requester); + void stop_stream(CameraRequester requester); + void request_image(CameraRequester requester); protected: uint32_t hash_base() override; @@ -91,6 +99,9 @@ class ESP32Camera : public Component, public EntityBase { camera_config_t config_{}; bool vertical_flip_{true}; bool horizontal_mirror_{true}; + bool aec2_{false}; + int ae_level_{0}; + uint32_t aec_value_{300}; int contrast_{0}; int brightness_{0}; int saturation_{0}; @@ -98,13 +109,14 @@ class ESP32Camera : public Component, public EntityBase { esp_err_t init_error_{ESP_OK}; std::shared_ptr current_image_; - uint32_t last_stream_request_{0}; - bool single_requester_{false}; + uint8_t single_requesters_{0}; + uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; CallbackManager)> new_image_callback_; uint32_t max_update_interval_{1000}; uint32_t idle_update_interval_{15000}; + uint32_t last_idle_request_{0}; uint32_t last_update_{0}; }; diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 653a274bf4..39b110bc85 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -14,7 +14,7 @@ namespace esphome { namespace esp32_camera_web_server { -static const int IMAGE_REQUEST_TIMEOUT = 2000; +static const int IMAGE_REQUEST_TIMEOUT = 5000; static const char *const TAG = "esp32_camera_web_server"; #define PART_BOUNDARY "123456789000000000000987654321" @@ -68,7 +68,7 @@ void CameraWebServer::setup() { httpd_register_uri_handler(this->httpd_, &uri); esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) { - if (this->running_) { + if (this->running_ && image->was_requested_by(esp32_camera::WEB_REQUESTER)) { this->image_ = std::move(image); xSemaphoreGive(this->semaphore_); } @@ -169,11 +169,9 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { uint32_t last_frame = millis(); uint32_t frames = 0; - while (res == ESP_OK && this->running_) { - if (esp32_camera::global_esp32_camera != nullptr) { - esp32_camera::global_esp32_camera->request_stream(); - } + esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::WEB_REQUESTER); + while (res == ESP_OK && this->running_) { auto image = this->wait_for_image_(); if (!image) { @@ -204,6 +202,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR)); } + esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER); + ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); return res; @@ -212,9 +212,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { esp_err_t res = ESP_OK; - if (esp32_camera::global_esp32_camera != nullptr) { - esp32_camera::global_esp32_camera->request_image(); - } + esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::WEB_REQUESTER); auto image = this->wait_for_image_(); @@ -233,9 +231,6 @@ esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); - if (res == ESP_OK) { - res = httpd_resp_set_hdr(req, CONTENT_LENGTH, esphome::to_string(image->get_data_length()).c_str()); - } if (res == ESP_OK) { res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length()); } diff --git a/esphome/components/esp32_can/__init__.py b/esphome/components/esp32_can/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py new file mode 100644 index 0000000000..7761418c6a --- /dev/null +++ b/esphome/components/esp32_can/canbus.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import canbus +from esphome.const import CONF_ID, CONF_RX_PIN, CONF_TX_PIN +from esphome.components.canbus import CanbusComponent, CanSpeed, CONF_BIT_RATE + +CODEOWNERS = ["@Sympatron"] +DEPENDENCIES = ["esp32"] + +esp32_can_ns = cg.esphome_ns.namespace("esp32_can") +esp32_can = esp32_can_ns.class_("ESP32Can", CanbusComponent) + +# Currently the driver only supports a subset of the bit rates defined in canbus +CAN_SPEEDS = { + "50KBPS": CanSpeed.CAN_50KBPS, + "100KBPS": CanSpeed.CAN_100KBPS, + "125KBPS": CanSpeed.CAN_125KBPS, + "250KBPS": CanSpeed.CAN_250KBPS, + "500KBPS": CanSpeed.CAN_500KBPS, + "1000KBPS": CanSpeed.CAN_1000KBPS, +} + +CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(esp32_can), + cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True), + cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await canbus.register_canbus(var, config) + + cg.add(var.set_rx(config[CONF_RX_PIN])) + cg.add(var.set_tx(config[CONF_TX_PIN])) diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp new file mode 100644 index 0000000000..baae683988 --- /dev/null +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -0,0 +1,123 @@ +#ifdef USE_ESP32 +#include "esp32_can.h" +#include "esphome/core/log.h" + +#include + +// WORKAROUND, because CAN_IO_UNUSED is just defined as (-1) in this version +// of the framework which does not work with -fpermissive +#undef CAN_IO_UNUSED +#define CAN_IO_UNUSED ((gpio_num_t) -1) + +namespace esphome { +namespace esp32_can { + +static const char *const TAG = "esp32_can"; + +static bool get_bitrate(canbus::CanSpeed bitrate, can_timing_config_t *t_config) { + switch (bitrate) { + case canbus::CAN_50KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_50KBITS(); + return true; + case canbus::CAN_100KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_100KBITS(); + return true; + case canbus::CAN_125KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_125KBITS(); + return true; + case canbus::CAN_250KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_250KBITS(); + return true; + case canbus::CAN_500KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_500KBITS(); + return true; + case canbus::CAN_1000KBPS: + *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_1MBITS(); + return true; + default: + return false; + } +} + +bool ESP32Can::setup_internal() { + can_general_config_t g_config = + CAN_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, CAN_MODE_NORMAL); + can_filter_config_t f_config = CAN_FILTER_CONFIG_ACCEPT_ALL(); + can_timing_config_t t_config; + + if (!get_bitrate(this->bit_rate_, &t_config)) { + // invalid bit rate + this->mark_failed(); + return false; + } + + // Install CAN driver + if (can_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + // Failed to install driver + this->mark_failed(); + return false; + } + + // Start CAN driver + if (can_start() != ESP_OK) { + // Failed to start driver + this->mark_failed(); + return false; + } + return true; +} + +canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAILTX; + } + + uint32_t flags = CAN_MSG_FLAG_NONE; + if (frame->use_extended_id) { + flags |= CAN_MSG_FLAG_EXTD; + } + if (frame->remote_transmission_request) { + flags |= CAN_MSG_FLAG_RTR; + } + + can_message_t message = { + .flags = flags, + .identifier = frame->can_id, + .data_length_code = frame->can_data_length_code, + }; + if (!frame->remote_transmission_request) { + memcpy(message.data, frame->data, frame->can_data_length_code); + } + + if (can_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) { + return canbus::ERROR_OK; + } else { + return canbus::ERROR_ALLTXBUSY; + } +} + +canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { + can_message_t message; + + if (can_receive(&message, 0) != ESP_OK) { + return canbus::ERROR_NOMSG; + } + + frame->can_id = message.identifier; + frame->use_extended_id = message.flags & CAN_MSG_FLAG_EXTD; + frame->remote_transmission_request = message.flags & CAN_MSG_FLAG_RTR; + frame->can_data_length_code = message.data_length_code; + + if (!frame->remote_transmission_request) { + size_t dlc = + message.data_length_code < canbus::CAN_MAX_DATA_LENGTH ? message.data_length_code : canbus::CAN_MAX_DATA_LENGTH; + memcpy(frame->data, message.data, dlc); + } + + return canbus::ERROR_OK; +} + +} // namespace esp32_can +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h new file mode 100644 index 0000000000..a428834f65 --- /dev/null +++ b/esphome/components/esp32_can/esp32_can.h @@ -0,0 +1,29 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/canbus/canbus.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace esp32_can { + +class ESP32Can : public canbus::Canbus { + public: + void set_rx(int rx) { rx_ = rx; } + void set_tx(int tx) { tx_ = tx; } + ESP32Can(){}; + + protected: + bool setup_internal() override; + canbus::Error send_message(struct canbus::CanFrame *frame) override; + canbus::Error read_message(struct canbus::CanFrame *frame) override; + + int rx_{-1}; + int tx_{-1}; +}; + +} // namespace esp32_can +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 22bebdfe98..788e7a9460 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -219,7 +219,7 @@ void ESP32ImprovComponent::dump_config() { void ESP32ImprovComponent::process_incoming_data_() { uint8_t length = this->incoming_data_[1]; - ESP_LOGD(TAG, "Processing bytes - %s", hexencode(this->incoming_data_).c_str()); + ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); if (this->incoming_data_.size() - 3 == length) { this->set_error_(improv::ERROR_NONE); improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 34c792499d..34a4a2fadb 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,4 +1,5 @@ import logging +import os from esphome.const import ( CONF_BOARD, @@ -14,6 +15,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv import esphome.codegen as cg +from esphome.helpers import copy_file_if_changed from .const import CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, esp8266_ns from .boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS @@ -158,6 +160,8 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") + cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) + conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") @@ -210,3 +214,14 @@ async def to_code(config): if ld_script is not None: cg.add_platformio_option("board_build.ldscript", ld_script) + + +# Called by writer.py +def copy_files(): + + dir = os.path.dirname(__file__) + post_build_file = os.path.join(dir, "post_build.py") + copy_file_if_changed( + post_build_file, + CORE.relative_build_path("post_build.py"), + ) diff --git a/esphome/components/esp8266/post_build.py b/esphome/components/esp8266/post_build.py new file mode 100644 index 0000000000..4dab1cbd27 --- /dev/null +++ b/esphome/components/esp8266/post_build.py @@ -0,0 +1,15 @@ +import shutil + +# pylint: disable=E0602 +Import("env") # noqa + + +def esp8266_copy_factory_bin(source, target, env): + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") + + shutil.copyfile(firmware_name, new_file_name) + + +# pylint: disable=E0602 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_factory_bin) # noqa diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 3c1b6e33e8..2ee5782ff6 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -79,7 +79,7 @@ void EZOSensor::loop() { if (buf[i] == ',') buf[i] = '\0'; - float val = parse_number((char *) &buf[1], sizeof(buf) - 2).value_or(0); + float val = parse_number((char *) &buf[1]).value_or(0); this->publish_state(val); } diff --git a/esphome/components/growatt_solar/__init__.py b/esphome/components/growatt_solar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp new file mode 100644 index 0000000000..ed7240ab6c --- /dev/null +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -0,0 +1,69 @@ +#include "growatt_solar.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace growatt_solar { + +static const char *const TAG = "growatt_solar"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 33; + +void GrowattSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } + +void GrowattSolar::on_modbus_data(const std::vector &data) { + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { + if (sensor == nullptr) + return; + float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit; + sensor->publish_state(value); + }; + + auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg1, size_t reg2, float unit) -> void { + float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) + + encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) * + unit; + if (sensor != nullptr) + sensor->publish_state(value); + }; + + publish_1_reg_sensor_state(this->inverter_status_, 0, 1); + + publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); +} + +void GrowattSolar::dump_config() { + ESP_LOGCONFIG(TAG, "GROWATT Solar:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); +} + +} // namespace growatt_solar +} // namespace esphome diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h new file mode 100644 index 0000000000..5356ac907a --- /dev/null +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace growatt_solar { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; + +class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { + public: + void update() override; + void on_modbus_data(const std::vector &data) override; + void dump_config() override; + + void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; } + + void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; } + void set_grid_active_power_sensor(sensor::Sensor *sensor) { this->grid_active_power_sensor_ = sensor; } + void set_pv_active_power_sensor(sensor::Sensor *sensor) { this->pv_active_power_sensor_ = sensor; } + + void set_today_production_sensor(sensor::Sensor *sensor) { this->today_production_ = sensor; } + void set_total_energy_production_sensor(sensor::Sensor *sensor) { this->total_energy_production_ = sensor; } + void set_inverter_module_temp_sensor(sensor::Sensor *sensor) { this->inverter_module_temp_ = sensor; } + + void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) { + this->phases_[phase].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor(uint8_t phase, sensor::Sensor *current_sensor) { + this->phases_[phase].current_sensor_ = current_sensor; + } + void set_active_power_sensor(uint8_t phase, sensor::Sensor *active_power_sensor) { + this->phases_[phase].active_power_sensor_ = active_power_sensor; + } + void set_voltage_sensor_pv(uint8_t pv, sensor::Sensor *voltage_sensor) { + this->pvs_[pv].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor_pv(uint8_t pv, sensor::Sensor *current_sensor) { + this->pvs_[pv].current_sensor_ = current_sensor; + } + void set_active_power_sensor_pv(uint8_t pv, sensor::Sensor *active_power_sensor) { + this->pvs_[pv].active_power_sensor_ = active_power_sensor; + } + + protected: + struct GrowattPhase { + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + } phases_[3]; + struct GrowattPV { + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + } pvs_[2]; + + sensor::Sensor *inverter_status_{nullptr}; + + sensor::Sensor *grid_frequency_sensor_{nullptr}; + sensor::Sensor *grid_active_power_sensor_{nullptr}; + + sensor::Sensor *pv_active_power_sensor_{nullptr}; + + sensor::Sensor *today_production_{nullptr}; + sensor::Sensor *total_energy_production_{nullptr}; + sensor::Sensor *inverter_module_temp_{nullptr}; +}; + +} // namespace growatt_solar +} // namespace esphome diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py new file mode 100644 index 0000000000..99936c33ee --- /dev/null +++ b/esphome/components/growatt_solar/sensor.py @@ -0,0 +1,201 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_CURRENT, + CONF_FREQUENCY, + CONF_ID, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_WATT, +) + +CONF_PHASE_A = "phase_a" +CONF_PHASE_B = "phase_b" +CONF_PHASE_C = "phase_c" + +CONF_ENERGY_PRODUCTION_DAY = "energy_production_day" +CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production" +CONF_TOTAL_GENERATION_TIME = "total_generation_time" +CONF_TODAY_GENERATION_TIME = "today_generation_time" +CONF_PV1 = "pv1" +CONF_PV2 = "pv2" +UNIT_KILOWATT_HOURS = "kWh" +UNIT_HOURS = "h" +UNIT_KOHM = "kΩ" +UNIT_MILLIAMPERE = "mA" + +CONF_INVERTER_STATUS = "inverter_status" +CONF_PV_ACTIVE_POWER = "pv_active_power" +CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" + + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@leeuwte"] + +growatt_solar_ns = cg.esphome_ns.namespace("growatt_solar") +GrowattSolar = growatt_solar_ns.class_( + "GrowattSolar", cg.PollingComponent, modbus.ModbusDevice +) + +PHASE_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), +} +PV_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), +} + +PHASE_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PHASE_SENSORS.items()} +) +PV_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()} +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GrowattSolar), + cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, + cv.Optional(CONF_PV1): PV_SCHEMA, + cv.Optional(CONF_PV2): PV_SCHEMA, + cv.Optional(CONF_INVERTER_STATUS): sensor.sensor_schema(), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PV_ACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + + if CONF_INVERTER_STATUS in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS]) + cg.add(var.set_inverter_status_sensor(sens)) + + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_grid_frequency_sensor(sens)) + + if CONF_ACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_ACTIVE_POWER]) + cg.add(var.set_grid_active_power_sensor(sens)) + + if CONF_PV_ACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_PV_ACTIVE_POWER]) + cg.add(var.set_pv_active_power_sensor(sens)) + + if CONF_ENERGY_PRODUCTION_DAY in config: + sens = await sensor.new_sensor(config[CONF_ENERGY_PRODUCTION_DAY]) + cg.add(var.set_today_production_sensor(sens)) + + if CONF_TOTAL_ENERGY_PRODUCTION in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_ENERGY_PRODUCTION]) + cg.add(var.set_total_energy_production_sensor(sens)) + + if CONF_INVERTER_MODULE_TEMP in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_MODULE_TEMP]) + cg.add(var.set_inverter_module_temp_sensor(sens)) + + for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]): + if phase not in config: + continue + + phase_config = config[phase] + for sensor_type in PHASE_SENSORS: + if sensor_type in phase_config: + sens = await sensor.new_sensor(phase_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor")(i, sens)) + + for i, pv in enumerate([CONF_PV1, CONF_PV2]): + if pv not in config: + continue + + pv_config = config[pv] + for sensor_type in pv_config: + if sensor_type in pv_config: + sens = await sensor.new_sensor(pv_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor_pv")(i, sens)) diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 60e8943e67..7186578a22 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -21,7 +21,9 @@ void HDC1080Component::setup() { }; if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { - this->mark_failed(); + // as instruction is same as powerup defaults (for now), interpret as warning if this fails + ESP_LOGW(TAG, "HDC1080 initial config instruction error"); + this->status_set_warning(); return; } } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 592e03f959..744ef5e527 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -109,8 +109,8 @@ def to_code(config): cg.add(var.set_protocol(config[CONF_PROTOCOL])) cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) - cg.add(var.set_max_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add(var.set_min_temperature(config[CONF_MAX_TEMPERATURE])) + cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) + cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) # PIO isn't updating releases, so referencing the release tag directly. See: # https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 774d6a0f91..e044e5fece 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -172,7 +172,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): if CONF_JSON in config: json_ = config[CONF_JSON] if isinstance(json_, Lambda): - args_ = args + [(cg.JsonObjectRef, "root")] + args_ = args + [(cg.JsonObject, "root")] lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 309977a915..a80d095835 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -115,8 +115,11 @@ void HttpRequestComponent::close() { } const char *HttpRequestComponent::get_string() { - static const String STR = this->client_.getString(); - return STR.c_str(); + // The static variable is here because HTTPClient::getString() returns a String on ESP32, and we need something to + // to keep a buffer alive. + static std::string str; + str = this->client_.getString().c_str(); + return str.c_str(); } } // namespace http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 9cc027b58d..a38bdf9c95 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -78,7 +78,7 @@ template class HttpRequestSendAction : public Action { void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } - void set_json(std::function json_func) { this->json_func_ = json_func; } + void set_json(std::function json_func) { this->json_func_ = json_func; } void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } @@ -118,17 +118,17 @@ template class HttpRequestSendAction : public Action { } protected: - void encode_json_(Ts... x, JsonObject &root) { + void encode_json_(Ts... x, JsonObject root) { for (const auto &item : this->json_) { auto val = item.second; root[item.first] = val.value(x...); } } - void encode_json_func_(Ts... x, JsonObject &root) { this->json_func_(x..., root); } + void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; std::map> headers_{}; std::map> json_{}; - std::function json_func_{nullptr}; + std::function json_func_{nullptr}; std::vector response_triggers_; }; diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index cb00260f43..71f6b1d15b 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include namespace esphome { namespace i2c { @@ -40,6 +42,20 @@ class I2CBus { return writev(address, &buf, 1); } virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) = 0; + + protected: + void i2c_scan_() { + for (uint8_t address = 8; address < 120; address++) { + auto err = writev(address, nullptr, 0); + if (err == ERROR_OK) { + scan_results_.emplace_back(address, true); + } else if (err == ERROR_UNKNOWN) { + scan_results_.emplace_back(address, false); + } + } + } + std::vector> scan_results_; + bool scan_{false}; }; } // namespace i2c diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 4afabbfa53..b605928692 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -2,6 +2,7 @@ #include "i2c_bus_arduino.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include #include @@ -24,9 +25,13 @@ void ArduinoI2CBus::setup() { wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer) #endif - wire_->begin(sda_pin_, scl_pin_); + wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); wire_->setClock(frequency_); initialized_ = true; + if (this->scan_) { + ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); + this->i2c_scan_(); + } } void ArduinoI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); @@ -45,22 +50,20 @@ void ArduinoI2CBus::dump_config() { break; } if (this->scan_) { - ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); - uint8_t found = 0; - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - if (err == ERROR_OK) { - ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); - found++; - } else if (err == ERROR_UNKNOWN) { - ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); - } - } - if (found == 0) { + ESP_LOGI(TAG, "Results from i2c bus scan:"); + if (scan_results_.empty()) { ESP_LOGI(TAG, "Found no i2c devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) + ESP_LOGI(TAG, "Found i2c device at address 0x%02X", s.first); + else + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } } } } + ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { // logging is only enabled with vv level, if warnings are shown the caller // should log them diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 82f043ef7d..f4151e4f37 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -34,7 +34,6 @@ class ArduinoI2CBus : public I2CBus, public Component { protected: TwoWire *wire_; - bool scan_; uint8_t sda_pin_; uint8_t scl_pin_; uint32_t frequency_; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index f7ecfe5f7c..109c3f890d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -3,6 +3,7 @@ #include "i2c_bus_esp_idf.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include namespace esphome { @@ -37,6 +38,10 @@ void IDFI2CBus::setup() { return; } initialized_ = true; + if (this->scan_) { + ESP_LOGV(TAG, "Scanning i2c bus for active devices..."); + this->i2c_scan_(); + } } void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); @@ -55,23 +60,20 @@ void IDFI2CBus::dump_config() { break; } if (this->scan_) { - ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); - uint8_t found = 0; - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - - if (err == ERROR_OK) { - ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address); - found++; - } else if (err == ERROR_UNKNOWN) { - ESP_LOGI(TAG, "Unknown error at address 0x%02X", address); - } - } - if (found == 0) { + ESP_LOGI(TAG, "Results from i2c bus scan:"); + if (scan_results_.empty()) { ESP_LOGI(TAG, "Found no i2c devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) + ESP_LOGI(TAG, "Found i2c device at address 0x%02X", s.first); + else + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } } } } + ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { // logging is only enabled with vv level, if warnings are shown the caller // should log them diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 13d996dbd8..d4b0626467 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -36,7 +36,6 @@ class IDFI2CBus : public I2CBus, public Component { protected: i2c_port_t port_; - bool scan_; uint8_t sda_pin_; bool sda_pullup_enabled_; uint8_t scl_pin_; diff --git a/esphome/components/ina260/__init__.py b/esphome/components/ina260/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ina260/ina260.cpp b/esphome/components/ina260/ina260.cpp new file mode 100644 index 0000000000..2f220e6a11 --- /dev/null +++ b/esphome/components/ina260/ina260.cpp @@ -0,0 +1,128 @@ +#include "ina260.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ina260 { + +static const char *const TAG = "ina260"; + +// | A0 | A1 | Address | +// | GND | GND | 0x40 | +// | GND | V_S+ | 0x41 | +// | GND | SDA | 0x42 | +// | GND | SCL | 0x43 | +// | V_S+ | GND | 0x44 | +// | V_S+ | V_S+ | 0x45 | +// | V_S+ | SDA | 0x46 | +// | V_S+ | SCL | 0x47 | +// | SDA | GND | 0x48 | +// | SDA | V_S+ | 0x49 | +// | SDA | SDA | 0x4A | +// | SDA | SCL | 0x4B | +// | SCL | GND | 0x4C | +// | SCL | V_S+ | 0x4D | +// | SCL | SDA | 0x4E | +// | SCL | SCL | 0x4F | + +static const uint8_t INA260_REGISTER_CONFIG = 0x00; +static const uint8_t INA260_REGISTER_CURRENT = 0x01; +static const uint8_t INA260_REGISTER_BUS_VOLTAGE = 0x02; +static const uint8_t INA260_REGISTER_POWER = 0x03; +static const uint8_t INA260_REGISTER_MASK_ENABLE = 0x06; +static const uint8_t INA260_REGISTER_ALERT_LIMIT = 0x07; +static const uint8_t INA260_REGISTER_MANUFACTURE_ID = 0xFE; +static const uint8_t INA260_REGISTER_DEVICE_ID = 0xFF; + +void INA260Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up INA260..."); + + // Reset device on setup + if (!this->write_byte_16(INA260_REGISTER_CONFIG, 0x8000)) { + this->error_code_ = DEVICE_RESET_FAILED; + this->mark_failed(); + return; + } + + delay(2); + + this->read_byte_16(INA260_REGISTER_MANUFACTURE_ID, &this->manufacture_id_); + this->read_byte_16(INA260_REGISTER_DEVICE_ID, &this->device_id_); + + if (this->manufacture_id_ != (uint16_t) 0x5449 || this->device_id_ != (uint16_t) 0x2270) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + if (!this->write_byte_16(INA260_REGISTER_CONFIG, (uint16_t) 0b0000001100000111)) { + this->error_code_ = FAILED_TO_UPDATE_CONFIGURATION; + this->mark_failed(); + return; + } +} + +void INA260Component::dump_config() { + ESP_LOGCONFIG(TAG, "INA260:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " Manufacture ID: 0x%x", this->manufacture_id_); + ESP_LOGCONFIG(TAG, " Device ID: 0x%x", this->device_id_); + + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Connected device does not match a known INA260 sensor"); + break; + case DEVICE_RESET_FAILED: + ESP_LOGE(TAG, "Device reset failed - Is the device connected?"); + break; + case FAILED_TO_UPDATE_CONFIGURATION: + ESP_LOGE(TAG, "Failed to update device configuration"); + break; + case NONE: + default: + break; + } +} + +void INA260Component::update() { + if (this->bus_voltage_sensor_ != nullptr) { + uint16_t raw_bus_voltage; + if (!this->read_byte_16(INA260_REGISTER_BUS_VOLTAGE, &raw_bus_voltage)) { + this->status_set_warning(); + return; + } + float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f; + this->bus_voltage_sensor_->publish_state(bus_voltage_v); + } + + if (this->current_sensor_ != nullptr) { + uint16_t raw_current; + if (!this->read_byte_16(INA260_REGISTER_CURRENT, &raw_current)) { + this->status_set_warning(); + return; + } + float current_a = int16_t(raw_current) * 0.00125f; + this->current_sensor_->publish_state(current_a); + } + + if (this->power_sensor_ != nullptr) { + uint16_t raw_power; + if (!this->read_byte_16(INA260_REGISTER_POWER, &raw_power)) { + this->status_set_warning(); + return; + } + float power_w = ((int16_t(raw_power) * 10.0f) / 1000.0f); + this->power_sensor_->publish_state(power_w); + } + + this->status_clear_warning(); +} + +} // namespace ina260 +} // namespace esphome diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h new file mode 100644 index 0000000000..8bad1cba6d --- /dev/null +++ b/esphome/components/ina260/ina260.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ina260 { + +class INA260Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { this->bus_voltage_sensor_ = bus_voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + + protected: + uint16_t manufacture_id_{0}; + uint16_t device_id_{0}; + + sensor::Sensor *bus_voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + + enum ErrorCode { + NONE, + COMMUNICATION_FAILED, + DEVICE_RESET_FAILED, + FAILED_TO_UPDATE_CONFIGURATION, + } error_code_{NONE}; +}; + +} // namespace ina260 +} // namespace esphome diff --git a/esphome/components/ina260/sensor.py b/esphome/components/ina260/sensor.py new file mode 100644 index 0000000000..048e713afa --- /dev/null +++ b/esphome/components/ina260/sensor.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@MrEditor97"] + +ina260_ns = cg.esphome_ns.namespace("ina260") +INA260Component = ina260_ns.class_( + "INA260Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(INA260Component), + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_BUS_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + cg.add(var.set_bus_voltage_sensor(sens)) + + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index e4c71ea717..dca764c6ed 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -6,11 +6,13 @@ from esphome.const import ( CONF_FULL_UPDATE_EVERY, CONF_ID, CONF_LAMBDA, + CONF_MODEL, CONF_PAGES, CONF_WAKEUP_PIN, ) DEPENDENCIES = ["i2c", "esp32"] +AUTO_LOAD = ["psram"] CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" @@ -40,6 +42,13 @@ Inkplate6 = inkplate6_ns.class_( "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer ) +InkplateModel = inkplate6_ns.enum("InkplateModel") + +MODELS = { + "inkplate_6": InkplateModel.INKPLATE_6, + "inkplate_10": InkplateModel.INKPLATE_10, +} + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { @@ -47,6 +56,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, + cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( + MODELS, lower=True, space="_" + ), # Control pins cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, @@ -110,6 +122,8 @@ async def to_code(config): cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + cg.add(var.set_model(config[CONF_MODEL])) + ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) cg.add(var.set_ckv_pin(ckv)) @@ -166,5 +180,3 @@ async def to_code(config): display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) cg.add(var.set_display_data_7_pin(display_data_7)) - - cg.add_build_flag("-DBOARD_HAS_PSRAM") diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 8a05836db9..e62e594a49 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -42,32 +42,32 @@ void Inkplate6::setup() { this->display(); } void Inkplate6::initialize_() { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint32_t buffer_size = this->get_buffer_length_(); + if (buffer_size == 0) + return; - if (this->partial_buffer_ != nullptr) { - free(this->partial_buffer_); // NOLINT - } - if (this->partial_buffer_2_ != nullptr) { - free(this->partial_buffer_2_); // NOLINT - } - if (this->buffer_ != nullptr) { - free(this->buffer_); // NOLINT - } + if (this->partial_buffer_ != nullptr) + allocator.deallocate(this->partial_buffer_, buffer_size); + if (this->partial_buffer_2_ != nullptr) + allocator.deallocate(this->partial_buffer_2_, buffer_size * 2); + if (this->buffer_ != nullptr) + allocator.deallocate(this->buffer_, buffer_size); - this->buffer_ = (uint8_t *) ps_malloc(buffer_size); + this->buffer_ = allocator.allocate(buffer_size); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); this->mark_failed(); return; } if (!this->greyscale_) { - this->partial_buffer_ = (uint8_t *) ps_malloc(buffer_size); + this->partial_buffer_ = allocator.allocate(buffer_size); if (this->partial_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate partial buffer for display!"); this->mark_failed(); return; } - this->partial_buffer_2_ = (uint8_t *) ps_malloc(buffer_size * 2); + this->partial_buffer_2_ = allocator.allocate(buffer_size * 2); if (this->partial_buffer_2_ == nullptr) { ESP_LOGE(TAG, "Could not allocate partial buffer 2 for display!"); this->mark_failed(); diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 56e95e95bb..2dac12a0c4 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -10,6 +10,11 @@ namespace esphome { namespace inkplate6 { +enum InkplateModel : uint8_t { + INKPLATE_6 = 0, + INKPLATE_10 = 1, +}; + class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { public: const uint8_t LUT2[16] = {0b10101010, 0b10101001, 0b10100110, 0b10100101, 0b10011010, 0b10011001, @@ -43,6 +48,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public void set_partial_updating(bool partial_updating) { this->partial_updating_ = partial_updating; } void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } + void set_model(InkplateModel model) { this->model_ = model; } + void set_display_data_0_pin(InternalGPIOPin *data) { this->display_data_0_pin_ = data; } void set_display_data_1_pin(InternalGPIOPin *data) { this->display_data_1_pin_ = data; } void set_display_data_2_pin(InternalGPIOPin *data) { this->display_data_2_pin_ = data; } @@ -101,9 +108,21 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public void pins_z_state_(); void pins_as_outputs_(); - int get_width_internal() override { return 800; } + int get_width_internal() override { + if (this->model_ == INKPLATE_6) + return 800; + else if (this->model_ == INKPLATE_10) + return 1200; + return 0; + } - int get_height_internal() override { return 600; } + int get_height_internal() override { + if (this->model_ == INKPLATE_6) + return 600; + else if (this->model_ == INKPLATE_10) + return 825; + return 0; + } size_t get_buffer_length_(); @@ -133,6 +152,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public bool greyscale_; bool partial_updating_; + InkplateModel model_; + InternalGPIOPin *display_data_0_pin_; InternalGPIOPin *display_data_1_pin_; InternalGPIOPin *display_data_2_pin_; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 2a398e5240..642116152c 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -23,31 +23,6 @@ void IntegrationSensor::setup() { this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); }); } void IntegrationSensor::dump_config() { LOG_SENSOR("", "Integration Sensor", this); } -std::string IntegrationSensor::unit_of_measurement() { - std::string suffix; - switch (this->time_) { - case INTEGRATION_SENSOR_TIME_MILLISECOND: - suffix = "ms"; - break; - case INTEGRATION_SENSOR_TIME_SECOND: - suffix = "s"; - break; - case INTEGRATION_SENSOR_TIME_MINUTE: - suffix = "min"; - break; - case INTEGRATION_SENSOR_TIME_HOUR: - suffix = "h"; - break; - case INTEGRATION_SENSOR_TIME_DAY: - suffix = "d"; - break; - } - std::string base = this->sensor_->get_unit_of_measurement(); - if (str_endswith(base, "/" + suffix)) { - return base.substr(0, base.size() - suffix.size() - 1); - } - return base + suffix; -} void IntegrationSensor::process_sensor_value_(float value) { const uint32_t now = millis(); const double old_value = this->last_value_; diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 437649c1dd..1d46973086 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -63,8 +63,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { this->last_save_ = now; this->rtc_.save(&result_f); } - std::string unit_of_measurement() override; - int8_t accuracy_decimals() override { return this->sensor_->get_accuracy_decimals() + 2; } sensor::Sensor *sensor_; IntegrationSensorTime time_; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 26c7c2871a..c35d42f385 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -2,7 +2,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import sensor -from esphome.const import CONF_ICON, CONF_ID, CONF_SENSOR, CONF_RESTORE +from esphome.const import ( + CONF_ICON, + CONF_ID, + CONF_SENSOR, + CONF_RESTORE, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, +) from esphome.core.entity_helpers import inherit_property_from integration_ns = cg.esphome_ns.namespace("integration") @@ -30,6 +37,18 @@ CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" CONF_MIN_SAVE_INTERVAL = "min_save_interval" + +def inherit_unit_of_measurement(uom, config): + suffix = config[CONF_TIME_UNIT] + if uom.endswith("/" + suffix): + return uom[0 : -len("/" + suffix)] + return uom + suffix + + +def inherit_accuracy_decimals(decimals, config): + return decimals + 2 + + CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(IntegrationSensor), @@ -51,11 +70,19 @@ FINAL_VALIDATE_SCHEMA = cv.All( { cv.Required(CONF_ID): cv.use_id(IntegrationSensor), cv.Optional(CONF_ICON): cv.icon, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): sensor.validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): sensor.validate_accuracy_decimals, cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), }, extra=cv.ALLOW_EXTRA, ), inherit_property_from(CONF_ICON, CONF_SENSOR), + inherit_property_from( + CONF_UNIT_OF_MEASUREMENT, CONF_SENSOR, transform=inherit_unit_of_measurement + ), + inherit_property_from( + CONF_ACCURACY_DECIMALS, CONF_SENSOR, transform=inherit_accuracy_decimals + ), ) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index fda0a552f1..6a0e4c50d2 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -7,12 +7,11 @@ json_ns = cg.esphome_ns.namespace("json") CONFIG_SCHEMA = cv.All( cv.Schema({}), - cv.only_with_arduino, ) @coroutine_with_priority(1.0) async def to_code(config): - cg.add_library("ottowinter/ArduinoJson-esphomelib", "5.13.3") + cg.add_library("bblanchon/ArduinoJson", "6.18.5") cg.add_define("USE_JSON") cg.add_global(json_ns.using) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 12c5beb73f..7e88fb6e59 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,8 +1,13 @@ -#ifdef USE_ARDUINO - #include "json_util.h" #include "esphome/core/log.h" +#ifdef USE_ESP8266 +#include +#endif +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace json { @@ -10,110 +15,49 @@ static const char *const TAG = "json"; static std::vector global_json_build_buffer; // NOLINT -const char *build_json(const json_build_t &f, size_t *length) { - global_json_buffer.clear(); - JsonObject &root = global_json_buffer.createObject(); +std::string build_json(const json_build_t &f) { + // Here we are allocating as much heap memory as available minus 2kb to be safe + // as we can not have a true dynamic sized document. + // The excess memory is freed below with `shrinkToFit()` +#ifdef USE_ESP8266 + const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP32) + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; +#endif + DynamicJsonDocument json_document(free_heap); + JsonObject root = json_document.to(); f(root); + json_document.shrinkToFit(); - // The Json buffer size gives us a good estimate for the required size. - // Usually, it's a bit larger than the actual required string size - // | JSON Buffer Size | String Size | - // Discovery | 388 | 351 | - // Discovery | 372 | 356 | - // Discovery | 336 | 311 | - // Discovery | 408 | 393 | - global_json_build_buffer.reserve(global_json_buffer.size() + 1); - size_t bytes_written = root.printTo(global_json_build_buffer.data(), global_json_build_buffer.capacity()); - - if (bytes_written >= global_json_build_buffer.capacity() - 1) { - global_json_build_buffer.reserve(root.measureLength() + 1); - bytes_written = root.printTo(global_json_build_buffer.data(), global_json_build_buffer.capacity()); - } - - *length = bytes_written; - return global_json_build_buffer.data(); + std::string output; + serializeJson(json_document, output); + return output; } -void parse_json(const std::string &data, const json_parse_t &f) { - global_json_buffer.clear(); - JsonObject &root = global_json_buffer.parseObject(data); - if (!root.success()) { +void parse_json(const std::string &data, const json_parse_t &f) { + // Here we are allocating as much heap memory as available minus 2kb to be safe + // as we can not have a true dynamic sized document. + // The excess memory is freed below with `shrinkToFit()` +#ifdef USE_ESP8266 + const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP32) + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; +#endif + + DynamicJsonDocument json_document(free_heap); + DeserializationError err = deserializeJson(json_document, data); + json_document.shrinkToFit(); + + JsonObject root = json_document.as(); + + if (err) { ESP_LOGW(TAG, "Parsing JSON failed."); return; } f(root); } -std::string build_json(const json_build_t &f) { - size_t len; - const char *c_str = build_json(f, &len); - return std::string(c_str, len); -} - -VectorJsonBuffer::String::String(VectorJsonBuffer *parent) : parent_(parent), start_(parent->size_) {} -void VectorJsonBuffer::String::append(char c) const { - char *last = static_cast(this->parent_->do_alloc(1)); - *last = c; -} -const char *VectorJsonBuffer::String::c_str() const { - this->append('\0'); - return &this->parent_->buffer_[this->start_]; -} -void VectorJsonBuffer::clear() { - for (char *block : this->free_blocks_) - free(block); // NOLINT - - this->size_ = 0; - this->free_blocks_.clear(); -} -VectorJsonBuffer::String VectorJsonBuffer::startString() { return {this}; } // NOLINT -void *VectorJsonBuffer::alloc(size_t bytes) { - // Make sure memory addresses are aligned - uint32_t new_size = round_size_up(this->size_); - this->resize(new_size); - return this->do_alloc(bytes); -} -void *VectorJsonBuffer::do_alloc(size_t bytes) { // NOLINT - const uint32_t begin = this->size_; - this->resize(begin + bytes); - return &this->buffer_[begin]; -} -void VectorJsonBuffer::resize(size_t size) { // NOLINT - if (size <= this->size_) { - this->size_ = size; - return; - } - - this->reserve(size); - this->size_ = size; -} -void VectorJsonBuffer::reserve(size_t size) { // NOLINT - if (size <= this->capacity_) - return; - - uint32_t target_capacity = this->capacity_; - if (this->capacity_ == 0) { - // lazily initialize with a reasonable size - target_capacity = JSON_OBJECT_SIZE(16); - } - while (target_capacity < size) - target_capacity *= 2; - - char *old_buffer = this->buffer_; - this->buffer_ = new char[target_capacity]; // NOLINT - if (old_buffer != nullptr && this->capacity_ != 0) { - this->free_blocks_.push_back(old_buffer); - memcpy(this->buffer_, old_buffer, this->capacity_); - } - this->capacity_ = target_capacity; -} - -size_t VectorJsonBuffer::size() const { return this->size_; } - -VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace json } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 577510e63a..57fe6107d8 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -1,68 +1,28 @@ #pragma once -#ifdef USE_ARDUINO - #include #include "esphome/core/helpers.h" + +#undef ARDUINOJSON_ENABLE_STD_STRING +#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT + #include namespace esphome { namespace json { /// Callback function typedef for parsing JsonObjects. -using json_parse_t = std::function; +using json_parse_t = std::function; /// Callback function typedef for building JsonObjects. -using json_build_t = std::function; +using json_build_t = std::function; /// Build a JSON string with the provided json build function. -const char *build_json(const json_build_t &f, size_t *length); - std::string build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. void parse_json(const std::string &data, const json_parse_t &f); -class VectorJsonBuffer : public ArduinoJson::Internals::JsonBufferBase { - public: - class String { - public: - String(VectorJsonBuffer *parent); - - void append(char c) const; - - const char *c_str() const; - - protected: - VectorJsonBuffer *parent_; - uint32_t start_; - }; - - void *alloc(size_t bytes) override; - - size_t size() const; - - void clear(); - - String startString(); // NOLINT - - protected: - void *do_alloc(size_t bytes); // NOLINT - - void resize(size_t size); // NOLINT - - void reserve(size_t size); // NOLINT - - char *buffer_{nullptr}; - size_t size_{0}; - size_t capacity_{0}; - std::vector free_blocks_; -}; - -extern VectorJsonBuffer global_json_buffer; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace json } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/kalman_combinator/__init__.py b/esphome/components/kalman_combinator/__init__.py new file mode 100644 index 0000000000..3356e61bb2 --- /dev/null +++ b/esphome/components/kalman_combinator/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/kalman_combinator/kalman_combinator.cpp b/esphome/components/kalman_combinator/kalman_combinator.cpp new file mode 100644 index 0000000000..d55f26126f --- /dev/null +++ b/esphome/components/kalman_combinator/kalman_combinator.cpp @@ -0,0 +1,82 @@ +#include "kalman_combinator.h" +#include "esphome/core/hal.h" +#include +#include + +namespace esphome { +namespace kalman_combinator { + +void KalmanCombinatorComponent::dump_config() { + ESP_LOGCONFIG("kalman_combinator", "Kalman Combinator:"); + ESP_LOGCONFIG("kalman_combinator", " Update variance: %f per ms", this->update_variance_value_); + ESP_LOGCONFIG("kalman_combinator", " Sensors:"); + for (const auto &sensor : this->sensors_) { + auto &entity = *sensor.first; + ESP_LOGCONFIG("kalman_combinator", " - %s", entity.get_name().c_str()); + } +} + +void KalmanCombinatorComponent::setup() { + for (const auto &sensor : this->sensors_) { + const auto stddev = sensor.second; + sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + } +} + +void KalmanCombinatorComponent::add_source(Sensor *sensor, std::function const &stddev) { + this->sensors_.emplace_back(sensor, stddev); +} + +void KalmanCombinatorComponent::add_source(Sensor *sensor, float stddev) { + this->add_source(sensor, std::function{[stddev](float) -> float { return stddev; }}); +} + +void KalmanCombinatorComponent::update_variance_() { + uint32_t now = millis(); + + // Variance increases by update_variance_ each millisecond + auto dt = now - this->last_update_; + auto dv = this->update_variance_value_ * dt; + this->variance_ += dv; + this->last_update_ = now; +} + +void KalmanCombinatorComponent::correct_(float value, float stddev) { + if (std::isnan(value) || std::isinf(stddev)) { + return; + } + + if (std::isnan(this->state_) || std::isinf(this->variance_)) { + this->state_ = value; + this->variance_ = stddev * stddev; + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(stddev); + } + return; + } + + this->update_variance_(); + + // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu + // Use the value with the smaller variance as mu1 to prevent precision errors + const bool this_first = this->variance_ < (stddev * stddev); + const float mu1 = this_first ? this->state_ : value; + const float mu2 = this_first ? value : this->state_; + + const float var1 = this_first ? this->variance_ : stddev * stddev; + const float var2 = this_first ? stddev * stddev : this->variance_; + + const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); + const float var = var1 - (var1 * var1) / (var1 + var2); + + // Update and publish state + this->state_ = mu; + this->variance_ = var; + + this->publish_state(mu); + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(std::sqrt(var)); + } +} +} // namespace kalman_combinator +} // namespace esphome diff --git a/esphome/components/kalman_combinator/kalman_combinator.h b/esphome/components/kalman_combinator/kalman_combinator.h new file mode 100644 index 0000000000..afbe3ece92 --- /dev/null +++ b/esphome/components/kalman_combinator/kalman_combinator.h @@ -0,0 +1,46 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include +#include + +namespace esphome { +namespace kalman_combinator { + +class KalmanCombinatorComponent : public Component, public sensor::Sensor { + public: + KalmanCombinatorComponent() = default; + + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + void dump_config() override; + void setup() override; + + void add_source(Sensor *sensor, std::function const &stddev); + void add_source(Sensor *sensor, float stddev); + void set_process_std_dev(float process_std_dev) { + this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; + } + void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } + + private: + void update_variance_(); + void correct_(float value, float stddev); + + // Source sensors and their error functions + std::vector>> sensors_; + + // Optional sensor for publishing the current error + sensor::Sensor *std_dev_sensor_{nullptr}; + + // Tick of the last update + uint32_t last_update_{0}; + // Change of the variance, per ms + float update_variance_value_{0.f}; + + // Best guess for the state and its variance + float state_{NAN}; + float variance_{INFINITY}; +}; +} // namespace kalman_combinator +} // namespace esphome diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py new file mode 100644 index 0000000000..9223f883b2 --- /dev/null +++ b/esphome/components/kalman_combinator/sensor.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_SOURCE, + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +kalman_combinator_ns = cg.esphome_ns.namespace("kalman_combinator") +KalmanCombinatorComponent = kalman_combinator_ns.class_( + "KalmanCombinatorComponent", cg.Component, sensor.Sensor +) + +CONF_ERROR = "error" +CONF_SOURCES = "sources" +CONF_PROCESS_STD_DEV = "process_std_dev" +CONF_STD_DEV = "std_dev" + + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(KalmanCombinatorComponent), + cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, + cv.Required(CONF_SOURCES): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), + } + ), + ), + cv.Optional(CONF_STD_DEV): sensor.SENSOR_SCHEMA, + } +) + +# Inherit some sensor values from the first source, for both the state and the error value +properties_to_inherit = [ + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, + # CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" +] +inherit_schema_for_state = [ + inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] +inherit_schema_for_std_dev = [ + inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] + +FINAL_VALIDATE_SCHEMA = cv.All( + CONFIG_SCHEMA.extend( + {cv.Required(CONF_ID): cv.use_id(KalmanCombinatorComponent)}, + extra=cv.ALLOW_EXTRA, + ), + *inherit_schema_for_state, + *inherit_schema_for_std_dev, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(var.set_process_std_dev(config[CONF_PROCESS_STD_DEV])) + for source_conf in config[CONF_SOURCES]: + source = await cg.get_variable(source_conf[CONF_SOURCE]) + error = await cg.templatable( + source_conf[CONF_ERROR], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, error)) + + if CONF_STD_DEV in config: + sens = await sensor.new_sensor(config[CONF_STD_DEV]) + cg.add(var.set_std_dev_sensor(sens)) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 03224d4c10..fe8a90b8db 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_RESTORE_MODE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_ON_STATE, CONF_TRIGGER_ID, CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE, @@ -37,6 +38,7 @@ from .types import ( # noqa AddressableLight, LightTurnOnTrigger, LightTurnOffTrigger, + LightStateTrigger, ) CODEOWNERS = ["@esphome/core"] @@ -69,6 +71,11 @@ LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).ex cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), } ), + cv.Optional(CONF_ON_STATE): auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightStateTrigger), + } + ), } ) @@ -151,6 +158,9 @@ async def setup_light_core_(light_var, output_var, config): for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) await auto.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) + await auto.build_automation(trigger, [], conf) if CONF_COLOR_CORRECT in config: cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT])) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 5091bae2d5..d404898edf 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -331,9 +331,10 @@ class AddressableFlickerEffect : public AddressableLightEffect { return; this->last_update_ = now; - fast_random_set_seed(random_uint32()); + uint32_t rng_state = random_uint32(); for (auto var : it) { - const uint8_t flicker = fast_random_8() % intensity; + rng_state = (rng_state * 0x9E3779B9) + 0x9E37; + const uint8_t flicker = (rng_state & 0xFF) % intensity; // scale down by random factor var = var.get() * (255 - flicker); diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 5ec2cb626a..b63fc93dc5 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -141,6 +141,13 @@ class LightTurnOffTrigger : public Trigger<> { } }; +class LightStateTrigger : public Trigger<> { + public: + LightStateTrigger(LightState *a_light) { + a_light->add_new_remote_values_callback([this]() { this->trigger(); }); + } +}; + // This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet // due to the template. It's just a temporary warning anyway. void addressableset_warn_about_scale(const char *field); diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 2e07d91046..c126859076 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -8,7 +8,7 @@ namespace light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { +void LightJSONSchema::dump_json(LightState &state, JsonObject root) { if (state.supports_effects()) root["effect"] = state.get_effect_name(); @@ -52,7 +52,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { if (values.get_color_mode() & ColorCapability::BRIGHTNESS) root["brightness"] = uint8_t(values.get_brightness() * 255); - JsonObject &color = root.createNestedObject("color"); + JsonObject color = root.createNestedObject("color"); if (values.get_color_mode() & ColorCapability::RGB) { color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); @@ -72,7 +72,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { } } -void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject &root) { +void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { if (root.containsKey("state")) { auto val = parse_on_off(root["state"]); switch (val) { @@ -95,7 +95,7 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } if (root.containsKey("color")) { - JsonObject &color = root["color"]; + JsonObject color = root["color"]; // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. float max_rgb = 0.0f; if (color.containsKey("r")) { @@ -140,7 +140,7 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } -void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject &root) { +void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { LightJSONSchema::parse_color_json(state, call, root); if (root.containsKey("flash")) { diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index 09a372f11c..c92dd7b655 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -14,12 +14,12 @@ namespace light { class LightJSONSchema { public: /// Dump the state of a light as JSON. - static void dump_json(LightState &state, JsonObject &root); + static void dump_json(LightState &state, JsonObject root); /// Parse the JSON state of a light to a LightCall. - static void parse_json(LightState &state, LightCall &call, JsonObject &root); + static void parse_json(LightState &state, LightCall &call, JsonObject root); protected: - static void parse_color_json(LightState &state, LightCall &call, JsonObject &root); + static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; } // namespace light diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index bc20cd5555..a453debd94 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -41,6 +41,7 @@ LightTurnOnTrigger = light_ns.class_( LightTurnOffTrigger = light_ns.class_( "LightTurnOffTrigger", automation.Trigger.template() ) +LightStateTrigger = light_ns.class_("LightStateTrigger", automation.Trigger.template()) # Effects LightEffect = light_ns.class_("LightEffect") diff --git a/esphome/components/mcp3204/__init__.py b/esphome/components/mcp3204/__init__.py new file mode 100644 index 0000000000..0536166e56 --- /dev/null +++ b/esphome/components/mcp3204/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +DEPENDENCIES = ["spi"] +MULTI_CONF = True +CODEOWNERS = ["@rsumner"] + +mcp3204_ns = cg.esphome_ns.namespace("mcp3204") +MCP3204 = mcp3204_ns.class_("MCP3204", cg.Component, spi.SPIDevice) + +CONF_REFERENCE_VOLTAGE = "reference_voltage" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MCP3204), + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } +).extend(spi.spi_device_schema(cs_pin_required=True)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp new file mode 100644 index 0000000000..44044349a3 --- /dev/null +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -0,0 +1,35 @@ +#include "mcp3204.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3204 { + +static const char *const TAG = "mcp3204"; + +float MCP3204::get_setup_priority() const { return setup_priority::HARDWARE; } + +void MCP3204::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp3204"); + this->spi_setup(); +} + +void MCP3204::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3204:"); + LOG_PIN(" CS Pin:", this->cs_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); +} + +float MCP3204::read_data(uint8_t pin) { + uint8_t adc_primary_config = 0b00000110 & 0b00000111; + uint8_t adc_secondary_config = pin << 6; + this->enable(); + this->transfer_byte(adc_primary_config); + uint8_t adc_primary_byte = this->transfer_byte(adc_secondary_config); + uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->disable(); + uint16_t digital_value = (adc_primary_byte << 8 | adc_secondary_byte) & 0b111111111111; + return float(digital_value) / 4096.000 * this->reference_voltage_; +} + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h new file mode 100644 index 0000000000..27261aa373 --- /dev/null +++ b/esphome/components/mcp3204/mcp3204.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp3204 { + +class MCP3204 : public Component, + public spi::SPIDevice { + public: + MCP3204() = default; + + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + float read_data(uint8_t pin); + + protected: + float reference_voltage_; +}; + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py new file mode 100644 index 0000000000..1d8701a91e --- /dev/null +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import CONF_ID, CONF_NUMBER +from .. import mcp3204_ns, MCP3204 + +AUTO_LOAD = ["voltage_sampler"] + +DEPENDENCIES = ["mcp3204"] + +MCP3204Sensor = mcp3204_ns.class_( + "MCP3204Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) +CONF_MCP3204_ID = "mcp3204_id" + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MCP3204Sensor), + cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=3), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_NUMBER], + ) + await cg.register_parented(var, config[CONF_MCP3204_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp new file mode 100644 index 0000000000..ce0fd25462 --- /dev/null +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -0,0 +1,23 @@ +#include "mcp3204_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3204 { + +static const char *const TAG = "mcp3204.sensor"; + +MCP3204Sensor::MCP3204Sensor(uint8_t pin) : pin_(pin) {} + +float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MCP3204Sensor::dump_config() { + LOG_SENSOR("", "MCP3204 Sensor", this); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + LOG_UPDATE_INTERVAL(this); +} +float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_); } +void MCP3204Sensor::update() { this->publish_state(this->sample()); } + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h new file mode 100644 index 0000000000..21c45590ab --- /dev/null +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +#include "../mcp3204.h" + +namespace esphome { +namespace mcp3204 { + +class MCP3204Sensor : public PollingComponent, + public Parented, + public sensor::Sensor, + public voltage_sampler::VoltageSampler { + public: + MCP3204Sensor(uint8_t pin); + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + uint8_t pin_; +}; + +} // namespace mcp3204 +} // namespace esphome diff --git a/esphome/components/mcp47a1/__init__.py b/esphome/components/mcp47a1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp47a1/mcp47a1.cpp b/esphome/components/mcp47a1/mcp47a1.cpp new file mode 100644 index 0000000000..58f3b2ac72 --- /dev/null +++ b/esphome/components/mcp47a1/mcp47a1.cpp @@ -0,0 +1,21 @@ +#include "mcp47a1.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp47a1 { + +static const char *const TAG = "mcp47a1"; + +void MCP47A1::dump_config() { + ESP_LOGCONFIG(TAG, "MCP47A1 Output:"); + LOG_I2C_DEVICE(this); +} + +void MCP47A1::write_state(float state) { + const uint8_t value = remap(state, 0.0f, 1.0f, 63, 0); + this->write_byte(0, value); +} + +} // namespace mcp47a1 +} // namespace esphome diff --git a/esphome/components/mcp47a1/mcp47a1.h b/esphome/components/mcp47a1/mcp47a1.h new file mode 100644 index 0000000000..5c02e062ad --- /dev/null +++ b/esphome/components/mcp47a1/mcp47a1.h @@ -0,0 +1,17 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace mcp47a1 { + +class MCP47A1 : public Component, public output::FloatOutput, public i2c::I2CDevice { + public: + void dump_config() override; + void write_state(float state) override; +}; + +} // namespace mcp47a1 +} // namespace esphome diff --git a/esphome/components/mcp47a1/output.py b/esphome/components/mcp47a1/output.py new file mode 100644 index 0000000000..60235107e9 --- /dev/null +++ b/esphome/components/mcp47a1/output.py @@ -0,0 +1,27 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import output, i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +mcp47a1_ns = cg.esphome_ns.namespace("mcp47a1") +MCP47A1 = mcp47a1_ns.class_("MCP47A1", output.FloatOutput, cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MCP47A1), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x2E)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await output.register_output(var, config) diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index c6ff783439..0528a87d0e 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -23,7 +23,7 @@ void MD5Digest::get_hex(char *output) { } } -bool MD5Digest::equals_bytes(const char *expected) { +bool MD5Digest::equals_bytes(const uint8_t *expected) { for (size_t i = 0; i < 16; i++) { if (expected[i] != this->digest_[i]) { return false; @@ -33,18 +33,10 @@ bool MD5Digest::equals_bytes(const char *expected) { } bool MD5Digest::equals_hex(const char *expected) { - for (size_t i = 0; i < 16; i++) { - auto high = parse_hex(expected[i * 2]); - auto low = parse_hex(expected[i * 2 + 1]); - if (!high.has_value() || !low.has_value()) { - return false; - } - auto value = (*high << 4) | *low; - if (value != this->digest_[i]) { - return false; - } - } - return true; + uint8_t parsed[16]; + if (!parse_hex(expected, parsed, 16)) + return false; + return equals_bytes(parsed); } } // namespace md5 diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index e40f419347..1c15c9e57d 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -44,7 +44,7 @@ class MD5Digest { void get_hex(char *output); /// Compare the digest against a provided byte-encoded digest (16 bytes). - bool equals_bytes(const char *expected); + bool equals_bytes(const uint8_t *expected); /// Compare the digest against a provided hex-encoded digest (32 bytes). bool equals_hex(const char *expected); diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index 34a9f8498e..a8b89f9b7b 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -23,12 +23,12 @@ class IrFollowMeData : public IrData { } /* TEMPERATURE */ - uint8_t temp() const { return this->data_[4] - 1; } - void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; } + uint8_t temp() const { return this->get_value_(4) - 1; } + void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } /* BEEPER */ - bool beeper() const { return this->data_[3] & 128; } - void set_beeper(bool val) { this->set_value_(3, 1, 7, val); } + bool beeper() const { return this->get_value_(3, 128); } + void set_beeper(bool val) { this->set_mask_(3, val, 128); } protected: static const uint8_t MAX_TEMP = 37; diff --git a/esphome/components/midea_ir/__init__.py b/esphome/components/midea_ir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py new file mode 100644 index 0000000000..140e4ee4e0 --- /dev/null +++ b/esphome/components/midea_ir/climate.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir", "coolix"] +CODEOWNERS = ["@dudanov"] + +midea_ir_ns = cg.esphome_ns.namespace("midea_ir") +MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MideaIR), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/midea_ir/midea_data.h b/esphome/components/midea_ir/midea_data.h new file mode 100644 index 0000000000..0f7e24907d --- /dev/null +++ b/esphome/components/midea_ir/midea_data.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/components/remote_base/midea_protocol.h" +#include "esphome/components/climate/climate_mode.h" + +namespace esphome { +namespace midea_ir { + +using climate::ClimateMode; +using climate::ClimateFanMode; +using remote_base::MideaData; + +class ControlData : public MideaData { + public: + // Default constructor (power: ON, mode: AUTO, fan: AUTO, temp: 25C) + ControlData() : MideaData({MIDEA_TYPE_CONTROL, 0x82, 0x48, 0xFF, 0xFF}) {} + // Copy from Base + ControlData(const MideaData &data) : MideaData(data) {} + + void set_temp(float temp); + float get_temp() const; + + void set_mode(ClimateMode mode); + ClimateMode get_mode() const; + + void set_fan_mode(ClimateFanMode mode); + ClimateFanMode get_fan_mode() const; + + void set_sleep_preset(bool value) { this->set_mask_(1, value, 64); } + bool get_sleep_preset() const { return this->get_value_(1, 64); } + + void set_fahrenheit(bool value) { this->set_mask_(2, value, 32); } + bool get_fahrenheit() const { return this->get_value_(2, 32); } + + void fix(); + + protected: + enum Mode : uint8_t { + MODE_COOL, + MODE_DRY, + MODE_AUTO, + MODE_HEAT, + MODE_FAN_ONLY, + }; + enum FanMode : uint8_t { + FAN_AUTO, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + }; + void set_fan_mode_(FanMode mode) { this->set_value_(1, mode, 3, 3); } + FanMode get_fan_mode_() const { return static_cast(this->get_value_(1, 3, 3)); } + void set_mode_(Mode mode) { this->set_value_(1, mode, 7); } + Mode get_mode_() const { return static_cast(this->get_value_(1, 7)); } + void set_power_(bool value) { this->set_mask_(1, value, 128); } + bool get_power_() const { return this->get_value_(1, 128); } +}; + +class FollowMeData : public MideaData { + public: + // Default constructor (temp: 30C, beeper: off) + FollowMeData() : MideaData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} + // Copy from Base + FollowMeData(const MideaData &data) : MideaData(data) {} + // Direct from temperature and beeper values + FollowMeData(uint8_t temp, bool beeper = false) : FollowMeData() { + this->set_temp(temp); + this->set_beeper(beeper); + } + + /* TEMPERATURE */ + uint8_t temp() const { return this->get_value_(4) - 1; } + void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } + + /* BEEPER */ + bool beeper() const { return this->get_value_(3, 128); } + void set_beeper(bool value) { this->set_mask_(3, value, 128); } + + protected: + static const uint8_t MAX_TEMP = 37; +}; + +class SpecialData : public MideaData { + public: + SpecialData(uint8_t code) : MideaData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} + static const uint8_t VSWING_STEP = 1; + static const uint8_t VSWING_TOGGLE = 2; + static const uint8_t TURBO_TOGGLE = 9; +}; + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp new file mode 100644 index 0000000000..5e507cbbb0 --- /dev/null +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -0,0 +1,201 @@ +#include "midea_ir.h" +#include "midea_data.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/components/coolix/coolix.h" + +namespace esphome { +namespace midea_ir { + +static const char *const TAG = "midea_ir.climate"; + +void ControlData::set_temp(float temp) { + uint8_t min; + if (this->get_fahrenheit()) { + min = MIDEA_TEMPF_MIN; + temp = esphome::clamp(celsius_to_fahrenheit(temp), MIDEA_TEMPF_MIN, MIDEA_TEMPF_MAX); + } else { + min = MIDEA_TEMPC_MIN; + temp = esphome::clamp(temp, MIDEA_TEMPC_MIN, MIDEA_TEMPC_MAX); + } + this->set_value_(2, lroundf(temp) - min, 31); +} + +float ControlData::get_temp() const { + const uint8_t temp = this->get_value_(2, 31); + if (this->get_fahrenheit()) + return fahrenheit_to_celsius(static_cast(temp + MIDEA_TEMPF_MIN)); + return static_cast(temp + MIDEA_TEMPC_MIN); +} + +void ControlData::fix() { + // In FAN_AUTO, modes COOL, HEAT and FAN_ONLY bit #5 in byte #1 must be set + const uint8_t value = this->get_value_(1, 31); + if (value == 0 || value == 3 || value == 4) + this->set_mask_(1, true, 32); + // In FAN_ONLY mode we need to set all temperature bits + if (this->get_mode_() == MODE_FAN_ONLY) + this->set_mask_(2, true, 31); +} + +void ControlData::set_mode(ClimateMode mode) { + switch (mode) { + case ClimateMode::CLIMATE_MODE_OFF: + this->set_power_(false); + return; + case ClimateMode::CLIMATE_MODE_COOL: + this->set_mode_(MODE_COOL); + break; + case ClimateMode::CLIMATE_MODE_DRY: + this->set_mode_(MODE_DRY); + break; + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + this->set_mode_(MODE_FAN_ONLY); + break; + case ClimateMode::CLIMATE_MODE_HEAT: + this->set_mode_(MODE_HEAT); + break; + default: + this->set_mode_(MODE_AUTO); + break; + } + this->set_power_(true); +} + +ClimateMode ControlData::get_mode() const { + if (!this->get_power_()) + return ClimateMode::CLIMATE_MODE_OFF; + switch (this->get_mode_()) { + case MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + case MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + case MODE_FAN_ONLY: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + case MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + default: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + } +} + +void ControlData::set_fan_mode(ClimateFanMode mode) { + switch (mode) { + case ClimateFanMode::CLIMATE_FAN_LOW: + this->set_fan_mode_(FAN_LOW); + break; + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + this->set_fan_mode_(FAN_MEDIUM); + break; + case ClimateFanMode::CLIMATE_FAN_HIGH: + this->set_fan_mode_(FAN_HIGH); + break; + default: + this->set_fan_mode_(FAN_AUTO); + break; + } +} + +ClimateFanMode ControlData::get_fan_mode() const { + switch (this->get_fan_mode_()) { + case FAN_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + case FAN_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + case FAN_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + default: + return ClimateFanMode::CLIMATE_FAN_AUTO; + } +} + +void MideaIR::control(const climate::ClimateCall &call) { + // swing and preset resets after unit powered off + if (call.get_mode() == climate::CLIMATE_MODE_OFF) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + this->preset = climate::CLIMATE_PRESET_NONE; + } else if (call.get_swing_mode().has_value() && ((*call.get_swing_mode() == climate::CLIMATE_SWING_OFF && + this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || + (*call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL && + this->swing_mode == climate::CLIMATE_SWING_OFF))) { + this->swing_ = true; + } else if (call.get_preset().has_value() && + ((*call.get_preset() == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || + (*call.get_preset() == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { + this->boost_ = true; + } + climate_ir::ClimateIR::control(call); +} + +void MideaIR::transmit_(MideaData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); +} + +void MideaIR::transmit_state() { + if (this->swing_) { + SpecialData data(SpecialData::VSWING_TOGGLE); + this->transmit_(data); + this->swing_ = false; + return; + } + if (this->boost_) { + SpecialData data(SpecialData::TURBO_TOGGLE); + this->transmit_(data); + this->boost_ = false; + return; + } + ControlData data; + data.set_fahrenheit(this->fahrenheit_); + data.set_temp(this->target_temperature); + data.set_mode(this->mode); + data.set_fan_mode(this->fan_mode.value_or(ClimateFanMode::CLIMATE_FAN_AUTO)); + data.set_sleep_preset(this->preset == climate::CLIMATE_PRESET_SLEEP); + data.fix(); + this->transmit_(data); +} + +bool MideaIR::on_receive(remote_base::RemoteReceiveData data) { + auto midea = remote_base::MideaProtocol().decode(data); + if (midea.has_value()) + return this->on_midea_(*midea); + return coolix::CoolixClimate::on_coolix(this, data); +} + +bool MideaIR::on_midea_(const MideaData &data) { + ESP_LOGV(TAG, "Decoded Midea IR data: %s", data.to_string().c_str()); + if (data.type() == MideaData::MIDEA_TYPE_CONTROL) { + const ControlData status = data; + if (status.get_mode() != climate::CLIMATE_MODE_FAN_ONLY) + this->target_temperature = status.get_temp(); + this->mode = status.get_mode(); + this->fan_mode = status.get_fan_mode(); + if (status.get_sleep_preset()) + this->preset = climate::CLIMATE_PRESET_SLEEP; + else if (this->preset == climate::CLIMATE_PRESET_SLEEP) + this->preset = climate::CLIMATE_PRESET_NONE; + this->publish_state(); + return true; + } + if (data.type() == MideaData::MIDEA_TYPE_SPECIAL) { + switch (data[1]) { + case SpecialData::VSWING_TOGGLE: + this->swing_mode = this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? climate::CLIMATE_SWING_OFF + : climate::CLIMATE_SWING_VERTICAL; + break; + case SpecialData::TURBO_TOGGLE: + this->preset = this->preset == climate::CLIMATE_PRESET_BOOST ? climate::CLIMATE_PRESET_NONE + : climate::CLIMATE_PRESET_BOOST; + break; + } + this->publish_state(); + return true; + } + + return false; +} + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/midea_ir/midea_ir.h b/esphome/components/midea_ir/midea_ir.h new file mode 100644 index 0000000000..b89b2a7efc --- /dev/null +++ b/esphome/components/midea_ir/midea_ir.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" +#include "midea_data.h" + +namespace esphome { +namespace midea_ir { + +// Temperature +const uint8_t MIDEA_TEMPC_MIN = 17; // Celsius +const uint8_t MIDEA_TEMPC_MAX = 30; // Celsius +const uint8_t MIDEA_TEMPF_MIN = 62; // Fahrenheit +const uint8_t MIDEA_TEMPF_MAX = 86; // Fahrenheit + +class MideaIR : public climate_ir::ClimateIR { + public: + MideaIR() + : climate_ir::ClimateIR( + MIDEA_TEMPC_MIN, MIDEA_TEMPC_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}, + {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + + /// Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = value ? 0.5f : 1.0f; + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + void transmit_(MideaData &data); + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool on_midea_(const MideaData &data); + bool fahrenheit_{false}; + bool swing_{false}; + bool boost_{false}; +}; + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 9524f9daf4..60ce50097c 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -69,7 +69,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint8_t data_len = raw[2]; uint8_t data_offset = 3; // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0x10) { + if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { data_offset = 2; data_len = 4; } @@ -181,7 +181,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address this->flow_control_pin_->digital_write(false); waiting_for_response = address; last_send_ = millis(); - ESP_LOGV(TAG, "Modbus write: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty(data).c_str()); } // Helper function for lambdas @@ -202,7 +202,7 @@ void Modbus::send_raw(const std::vector &payload) { if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); waiting_for_response = payload[0]; - ESP_LOGV(TAG, "Modbus write raw: %s", hexencode(payload).c_str()); + ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty(payload).c_str()); last_send_ = millis(); } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index b927faf9a7..f919cb0678 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -47,11 +47,16 @@ MODBUS_FUNCTION_CODE = { ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") -MODBUS_REGISTER_TYPE = { + +MODBUS_WRITE_REGISTER_TYPE = { "custom": ModbusRegisterType.CUSTOM, "coil": ModbusRegisterType.COIL, - "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "holding": ModbusRegisterType.HOLDING, +} + +MODBUS_REGISTER_TYPE = { + **MODBUS_WRITE_REGISTER_TYPE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "read": ModbusRegisterType.READ, } diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 99d56fed67..557d76479d 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -57,4 +57,4 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - await add_modbus_base_properties(var, config, ModbusBinarySensor, cg.float_, bool) + await add_modbus_base_properties(var, config, ModbusBinarySensor, bool, bool) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index c516d6b916..21afbc7053 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -31,12 +31,11 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, void dump_config() override; - using transform_func_t = - optional(ModbusBinarySensor *, bool, const std::vector &)>>; + using transform_func_t = std::function(ModbusBinarySensor *, bool, const std::vector &)>; void set_template(transform_func_t &&f) { this->transform_func_ = f; } protected: - transform_func_t transform_func_{nullopt}; + optional transform_func_{nullopt}; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 8b96c20691..d07a6d5335 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -24,15 +24,22 @@ bool ModbusController::send_next_command_() { if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) { auto &command = command_queue_.front(); - ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, - command->register_address, command->register_count); - command->send(); - this->last_command_timestamp_ = millis(); - // remove from queue if no handler is defined or command was sent too often - if (!command->on_data_func || command->send_countdown < 1) { - ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X countdown=%d removed from queue after send", - this->address_, command->register_address, command->send_countdown); + // remove from queue if command was sent too often + if (command->send_countdown < 1) { + ESP_LOGD( + TAG, + "Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue", + this->address_, command->register_address, command->send_countdown); command_queue_.pop_front(); + } else { + ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, + command->register_address, command->register_count); + command->send(); + this->last_command_timestamp_ = millis(); + // remove from queue if no handler is defined + if (!command->on_data_func) { + command_queue_.pop_front(); + } } } return (!command_queue_.empty()); @@ -72,36 +79,28 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ } } -std::map::iterator ModbusController::find_register_(ModbusRegisterType register_type, - uint16_t start_address) { - auto vec_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { +SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { + auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); }); - if (vec_it == register_ranges_.end()) { - ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address); + if (reg_it == register_ranges_.end()) { + ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address); } else { - auto map_it = sensormap_.find(vec_it->first_sensorkey); - if (map_it == sensormap_.end()) { - ESP_LOGE(TAG, "No sensor found in at start_address : 0x%X (0x%llX)", start_address, vec_it->first_sensorkey); - } else { - return sensormap_.find(vec_it->first_sensorkey); - } + return reg_it->sensors; } + // not found - return std::end(sensormap_); + return {}; } void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); - auto map_it = find_register_(register_type, start_address); // loop through all sensors with the same start address - while (map_it != sensormap_.end() && map_it->second->start_address == start_address) { - if (map_it->second->register_type == register_type) { - map_it->second->parse_and_publish(data); - } - map_it++; + auto sensors = find_sensors_(register_type, start_address); + for (auto sensor : sensors) { + sensor->parse_and_publish(data); } } @@ -110,7 +109,7 @@ void ModbusController::queue_command(const ModbusCommandItem &command) { // not very effective but the queue is never really large for (auto &item : command_queue_) { if (item->register_address == command.register_address && item->register_count == command.register_count && - item->register_type == command.register_type) { + item->register_type == command.register_type && item->function_code == command.function_code) { ESP_LOGW(TAG, "Duplicate modbus command found"); // update the payload of the queued command // replaces a previous command @@ -127,15 +126,16 @@ void ModbusController::update_range_(RegisterRange &r) { if (r.skip_updates_counter == 0) { // if a custom command is used the user supplied custom_data is only available in the SensorItem. if (r.register_type == ModbusRegisterType::CUSTOM) { - auto it = this->find_register_(r.register_type, r.start_address); - if (it != sensormap_.end()) { + auto sensors = this->find_sensors_(r.register_type, r.start_address); + if (!sensors.empty()) { + auto sensor = sensors.cbegin(); auto command_item = ModbusCommandItem::create_custom_command( - this, it->second->custom_data, + this, (*sensor)->custom_data, [this](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data); }); - command_item.register_address = it->second->start_address; - command_item.register_count = it->second->register_count; + command_item.register_address = (*sensor)->start_address; + command_item.register_count = (*sensor)->register_count; command_item.function_code = ModbusFunctionCode::CUSTOM; queue_command(command_item); } @@ -164,102 +164,110 @@ void ModbusController::update() { } } -// walk through the sensors and determine the registerranges to read +// walk through the sensors and determine the register ranges to read size_t ModbusController::create_register_ranges_() { register_ranges_.clear(); - uint8_t n = 0; - if (sensormap_.empty()) { + if (sensorset_.empty()) { + ESP_LOGW(TAG, "No sensors registered"); return 0; } - auto ix = sensormap_.begin(); - auto prev = ix; - int total_register_count = 0; - uint16_t current_start_address = ix->second->start_address; - uint8_t buffer_offset = ix->second->offset; - uint8_t skip_updates = ix->second->skip_updates; - auto first_sensorkey = ix->second->getkey(); - total_register_count = 0; - while (ix != sensormap_.end()) { - ESP_LOGV(TAG, "Register: 0x%X %d %d 0x%llx (%d) buffer_offset = %d (0x%X) skip=%u", ix->second->start_address, - ix->second->register_count, ix->second->offset, ix->second->getkey(), total_register_count, buffer_offset, - buffer_offset, ix->second->skip_updates); - // if this is a sequential address based on number of registers and address of previous sensor - // convert to an offset to the previous sensor (address 0x101 becomes address 0x100 offset 2 bytes) - if (!ix->second->force_new_range && total_register_count >= 0 && - prev->second->register_type == ix->second->register_type && - prev->second->start_address + total_register_count == ix->second->start_address && - prev->second->start_address < ix->second->start_address) { - ix->second->start_address = prev->second->start_address; - ix->second->offset += prev->second->offset + prev->second->get_register_size(); + // iterator is sorted see SensorItemsComparator for details + auto ix = sensorset_.begin(); + RegisterRange r = {}; + uint8_t buffer_offset = 0; + SensorItem *prev = nullptr; + while (ix != sensorset_.end()) { + SensorItem *curr = *ix; - // replace entry in sensormap_ - auto const value = ix->second; - sensormap_.erase(ix); - sensormap_.insert({value->getkey(), value}); - // move iterator back to new element - ix = sensormap_.find(value->getkey()); // next(prev, 1); - } - if (current_start_address != ix->second->start_address || - // ( prev->second->start_address + prev->second->offset != ix->second->start_address) || - ix->second->register_type != prev->second->register_type) { - // Difference doesn't match so we have a gap - if (n > 0) { - RegisterRange r; - r.start_address = current_start_address; - r.register_count = total_register_count; - if (prev->second->register_type == ModbusRegisterType::COIL || - prev->second->register_type == ModbusRegisterType::DISCRETE_INPUT) { - r.register_count = prev->second->offset + 1; - } - r.register_type = prev->second->register_type; - r.first_sensorkey = first_sensorkey; - r.skip_updates = skip_updates; - r.skip_updates_counter = 0; - ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); - register_ranges_.push_back(r); - } - skip_updates = ix->second->skip_updates; - current_start_address = ix->second->start_address; - first_sensorkey = ix->second->getkey(); - total_register_count = ix->second->register_count; - buffer_offset = ix->second->offset; - n = 1; + ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, + curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr); + + if (r.register_count == 0) { + // this is the first register in range + r.start_address = curr->start_address; + r.register_count = curr->register_count; + r.register_type = curr->register_type; + r.sensors.insert(curr); + r.skip_updates = curr->skip_updates; + r.skip_updates_counter = 0; + buffer_offset = curr->get_register_size(); + + ESP_LOGV(TAG, "Started new range"); } else { - n++; - if (ix->second->offset != prev->second->offset || n == 1) { - total_register_count += ix->second->register_count; - buffer_offset += ix->second->get_register_size(); + // this is not the first register in range so it might be possible + // to reuse the last register or extend the current range + if (!curr->force_new_range && r.register_type == curr->register_type && + curr->register_type != ModbusRegisterType::CUSTOM) { + if (curr->start_address == (r.start_address + r.register_count - prev->register_count) && + curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) { + // this register can re-use the data from the previous register + + // remove this sensore because start_address is changed (sort-order) + ix = sensorset_.erase(ix); + + curr->start_address = r.start_address; + curr->offset += prev->offset; + + sensorset_.insert(curr); + // move iterator backwards because it will be incremented later + ix--; + + ESP_LOGV(TAG, "Re-use previous register - change to register: 0x%X %d offset=%u", curr->start_address, + curr->register_count, curr->offset); + } else if (curr->start_address == (r.start_address + r.register_count)) { + // this register can extend the current range + + // remove this sensore because start_address is changed (sort-order) + ix = sensorset_.erase(ix); + + curr->start_address = r.start_address; + curr->offset += buffer_offset; + buffer_offset += curr->get_register_size(); + r.register_count += curr->register_count; + + sensorset_.insert(curr); + // move iterator backwards because it will be incremented later + ix--; + + ESP_LOGV(TAG, "Extend range - change to register: 0x%X %d offset=%u", curr->start_address, + curr->register_count, curr->offset); + } } + } + + if (curr->start_address == r.start_address) { // use the lowest non zero value for the whole range // Because zero is the default value for skip_updates it is excluded from getting the min value. - if (ix->second->skip_updates != 0) { - if (skip_updates != 0) { - skip_updates = std::min(skip_updates, ix->second->skip_updates); + if (curr->skip_updates != 0) { + if (r.skip_updates != 0) { + r.skip_updates = std::min(r.skip_updates, curr->skip_updates); } else { - skip_updates = ix->second->skip_updates; + r.skip_updates = curr->skip_updates; } } + + // add sensor to this range + r.sensors.insert(curr); + + ix++; + } else { + ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + register_ranges_.push_back(r); + r = {}; + buffer_offset = 0; + // do not increment the iterator here because the current sensor has to be re-evaluated } - prev = ix++; + + prev = curr; } - // Add the last range - if (n > 0) { - RegisterRange r; - r.start_address = current_start_address; - // r.register_count = prev->second->offset>>1 + prev->second->get_register_size(); - r.register_count = total_register_count; - if (prev->second->register_type == ModbusRegisterType::COIL || - prev->second->register_type == ModbusRegisterType::DISCRETE_INPUT) { - r.register_count = prev->second->offset + 1; - } - r.register_type = prev->second->register_type; - r.first_sensorkey = first_sensorkey; - r.skip_updates = skip_updates; - r.skip_updates_counter = 0; + + if (r.register_count > 0) { + // Add the last range ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); register_ranges_.push_back(r); } + return register_ranges_.size(); } @@ -268,9 +276,15 @@ void ModbusController::dump_config() { ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); - for (auto &it : sensormap_) { - ESP_LOGCONFIG("TAG", " Sensor 0x%llX start=0x%X count=%d size=%d", it.second->getkey(), it.second->start_address, - it.second->register_count, it.second->get_register_size()); + for (auto &it : sensorset_) { + ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d", + static_cast(it->register_type), it->start_address, it->offset, it->register_count, + it->get_register_size()); + } + ESP_LOGCONFIG(TAG, "ranges"); + for (auto &it : register_ranges_) { + ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), + it.start_address, it.register_count, it.skip_updates); } #endif } @@ -294,11 +308,11 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data(data, 0), get_data(data, 1)); } -void ModbusController::dump_sensormap_() { - ESP_LOGV("modbuscontroller.h", "sensormap"); - for (auto &it : sensormap_) { - ESP_LOGV("modbuscontroller.h", " Sensor 0x%llX start=0x%X count=%d size=%d", it.second->getkey(), - it.second->start_address, it.second->register_count, it.second->get_register_size()); +void ModbusController::dump_sensors_() { + ESP_LOGV(TAG, "sensors"); + for (auto &it : sensorset_) { + ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count, + it->get_register_size(), it->offset); } } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index f4948e6ff9..6dbabac71e 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -6,7 +6,7 @@ #include "esphome/components/modbus/modbus.h" #include -#include +#include #include #include @@ -37,7 +37,7 @@ enum class ModbusFunctionCode { READ_FIFO_QUEUE = 0x18, // not implemented }; -enum class ModbusRegisterType : int { +enum class ModbusRegisterType : uint8_t { CUSTOM = 0x0, COIL = 0x01, DISCRETE_INPUT = 0x02, @@ -62,15 +62,6 @@ enum class SensorValueType : uint8_t { FP32_R = 0xD }; -struct RegisterRange { - uint16_t start_address; - ModbusRegisterType register_type; - uint8_t register_count; - uint8_t skip_updates; // the config value - uint64_t first_sensorkey; - uint8_t skip_updates_counter; // the running value -} __attribute__((packed)); - inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { switch (reg_type) { case ModbusRegisterType::COIL: @@ -108,18 +99,6 @@ inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_ } } -/** All sensors are stored in a map - * to enable binary sensors for values encoded as bits in the same register the key of each sensor - * the key is a 64 bit integer that combines the register properties - * sensormap_ is sorted by this key. The key ensures the correct order when creating consequtive ranges - * Format: function_code (8 bit) | start address (16 bit)| offset (8bit)| bitmask (32 bit) - */ -inline uint64_t calc_key(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset = 0, - uint32_t bitmask = 0) { - return uint64_t((uint16_t(register_type) << 24) + (uint32_t(start_address) << 8) + (offset & 0xFF)) << 32 | bitmask; -} -inline uint16_t register_from_key(uint64_t key) { return (key >> 40) & 0xFFFF; } - inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } /** Get a byte from a hex string @@ -250,7 +229,6 @@ class SensorItem { virtual void parse_and_publish(const std::vector &data) = 0; void set_custom_data(const std::vector &data) { custom_data = data; } - uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } size_t virtual get_register_size() const { if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) return 1; @@ -271,6 +249,48 @@ class SensorItem { bool force_new_range{false}; }; +// ModbusController::create_register_ranges_ tries to optimize register range +// for this the sensors must be ordered by register_type, start_address and bitmask +class SensorItemsComparator { + public: + bool operator()(const SensorItem *lhs, const SensorItem *rhs) const { + // first sort according to register type + if (lhs->register_type != rhs->register_type) { + return lhs->register_type < rhs->register_type; + } + + // ensure that sensor with force_new_range set are before the others + if (lhs->force_new_range != rhs->force_new_range) { + return lhs->force_new_range > rhs->force_new_range; + } + + // sort by start address + if (lhs->start_address != rhs->start_address) { + return lhs->start_address < rhs->start_address; + } + + // sort by offset (ensures update of sensors in ascending order) + if (lhs->offset != rhs->offset) { + return lhs->offset < rhs->offset; + } + + // The pointer to the sensor is used last to ensure that + // multiple sensors with the same values can be added with a stable sort order. + return lhs < rhs; + } +}; + +using SensorSet = std::set; + +struct RegisterRange { + uint16_t start_address; + ModbusRegisterType register_type; + uint8_t register_count; + uint8_t skip_updates; // the config value + SensorSet sensors; // all sensors of this range + uint8_t skip_updates_counter; // the running value +}; + class ModbusCommandItem { public: static const size_t MAX_PAYLOAD_BYTES = 240; @@ -382,8 +402,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// queues a modbus command in the send queue void queue_command(const ModbusCommandItem &command); /// Registers a sensor with the controller. Called by esphomes code generator - void add_sensor_item(SensorItem *item) { sensormap_[item->getkey()] = item; } - /// called when a modbus response was prased without errors + void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } + /// called when a modbus response was parsed without errors void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; @@ -400,7 +420,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// parse sensormap_ and create range of sequential addresses size_t create_register_ranges_(); // find register in sensormap. Returns iterator with all registers having the same start address - std::map::iterator find_register_(ModbusRegisterType register_type, uint16_t start_address); + SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const; /// submit the read command for the address range to the send queue void update_range_(RegisterRange &r); /// parse incoming modbus data @@ -410,10 +430,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// get the number of queued modbus commands (should be mostly empty) size_t get_command_queue_length_() { return command_queue_.size(); } /// dump the parsed sensormap for diagnostics - void dump_sensormap_(); + void dump_sensors_(); /// Collection of all sensors for this component - /// see calc_key how the key is contructed - std::map sensormap_; + SensorSet sensorset_; /// Continous range of modbus registers std::vector register_ranges_; /// Hold the pending requests to be sent diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index e5afd0c611..5e977f5df4 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -8,7 +8,7 @@ namespace modbus_controller { static const char *const TAG = "modbus.number"; void ModbusNumber::parse_and_publish(const std::vector &data) { - float result = payload_to_float(data, *this); + float result = payload_to_float(data, *this) / multiply_by_; // Is there a lambda registered // call it with the pre converted value and the raw data array @@ -52,7 +52,7 @@ void ModbusNumber::control(float value) { ESP_LOGD(TAG, "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", - this->get_name().c_str(), this->start_address, this->register_count, write_value, write_value); + this->get_name().c_str(), this->start_address, this->register_count, value, write_value); // Create and send the write command ModbusCommandItem write_cmd; diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index a26d05a18b..1bf989ce8b 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output - from esphome.const import ( CONF_ADDRESS, CONF_ID, @@ -11,13 +10,14 @@ from esphome.const import ( from .. import ( modbus_controller_ns, modbus_calc_properties, - validate_modbus_register, ModbusItemBaseSchema, SensorItem, + SENSOR_VALUE_TYPE, ) from ..const import ( CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, @@ -27,45 +27,83 @@ DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] -ModbusOutput = modbus_controller_ns.class_( - "ModbusOutput", cg.Component, output.FloatOutput, SensorItem +ModbusFloatOutput = modbus_controller_ns.class_( + "ModbusFloatOutput", cg.Component, output.FloatOutput, SensorItem +) +ModbusBinaryOutput = modbus_controller_ns.class_( + "ModbusBinaryOutput", cg.Component, output.BinaryOutput, SensorItem ) -CONFIG_SCHEMA = cv.All( - output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( - { - cv.GenerateID(): cv.declare_id(ModbusOutput), - cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, - cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, - } - ), - validate_modbus_register, + +CONFIG_SCHEMA = cv.typed_schema( + { + "coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusBinaryOutput), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + "holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusFloatOutput), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( + SENSOR_VALUE_TYPE + ), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + }, + lower=True, + key=CONF_REGISTER_TYPE, + default_type="holding", ) async def to_code(config): byte_offset, reg_count = modbus_calc_properties(config) - var = cg.new_Pvariable( - config[CONF_ID], - config[CONF_ADDRESS], - byte_offset, - config[CONF_VALUE_TYPE], - reg_count, - ) + # Binary Output + if config[CONF_REGISTER_TYPE] == "coil": + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + ) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusBinaryOutput.operator("ptr"), "item"), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), + ], + return_type=cg.optional.template(bool), + ) + # Float Output + else: + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + config[CONF_VALUE_TYPE], + reg_count, + ) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusFloatOutput.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) await output.register_output(var, config) - cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) cg.add(var.set_parent(parent)) if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_WRITE_LAMBDA], - [ - (ModbusOutput.operator("ptr"), "item"), - (cg.float_, "x"), - (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), - ], - return_type=cg.optional.template(float), - ) cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index 4c2e5775b9..b647312f52 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -1,18 +1,17 @@ #include #include "modbus_output.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace modbus_controller { static const char *const TAG = "modbus_controller.output"; -void ModbusOutput::setup() {} - /** Write a value to the device * */ -void ModbusOutput::write_state(float value) { +void ModbusFloatOutput::write_state(float value) { std::vector data; auto original_value = value; // Is there are lambda configured? @@ -39,7 +38,6 @@ void ModbusOutput::write_state(float value) { ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", this->start_address, this->register_count, value, original_value); - // Create and send the write command // Create and send the write command ModbusCommandItem write_cmd; if (this->register_count == 1 && !this->use_write_multiple_) { @@ -51,11 +49,62 @@ void ModbusOutput::write_state(float value) { parent_->queue_command(write_cmd); } -void ModbusOutput::dump_config() { +void ModbusFloatOutput::dump_config() { ESP_LOGCONFIG(TAG, "Modbus Float Output:"); LOG_FLOAT_OUTPUT(this); - ESP_LOGCONFIG(TAG, "Modbus device start address=0x%X register count=%d value type=%hhu", this->start_address, - this->register_count, this->sensor_value_type); + ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address); + ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count); + ESP_LOGCONFIG(TAG, " Value type: %d", static_cast(this->sensor_value_type)); +} + +// ModbusBinaryOutput +void ModbusBinaryOutput::write_state(bool state) { + // This will be called every time the user requests a state change. + ModbusCommandItem cmd; + std::vector data; + + // Is there are lambda configured? + if (this->write_transform_func_.has_value()) { + // data is passed by reference + // the lambda can fill the empty vector directly + // in that case the return value is ignored + auto val = (*this->write_transform_func_)(this, state, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + state = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } + if (!data.empty()) { + ESP_LOGV(TAG, "Modbus binary output write raw: %s", format_hex_pretty(data).c_str()); + cmd = ModbusCommandItem::create_custom_command( + this->parent_, data, + [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->parent_->on_write_register_response(cmd.register_type, this->start_address, data); + }); + } else { + ESP_LOGV(TAG, "Write new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state), + (int) this->register_type, this->start_address, this->offset); + + // offset for coil and discrete inputs is the coil/register number not bytes + if (this->use_write_multiple_) { + std::vector states{state}; + cmd = ModbusCommandItem::create_write_multiple_coils(parent_, this->start_address + this->offset, states); + } else { + cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); + } + } + this->parent_->queue_command(cmd); +} + +void ModbusBinaryOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus Binary Output:"); + LOG_BINARY_OUTPUT(this); + ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address); + ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count); + ESP_LOGCONFIG(TAG, " Value type: %d", static_cast(this->sensor_value_type)); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 78d3474ad6..6237805d24 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -7,11 +7,9 @@ namespace esphome { namespace modbus_controller { -using value_to_data_t = std::function(float); - -class ModbusOutput : public output::FloatOutput, public Component, public SensorItem { +class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem { public: - ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) + ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) : output::FloatOutput(), Component() { this->register_type = ModbusRegisterType::HOLDING; this->start_address = start_address; @@ -23,7 +21,6 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor this->start_address += offset; this->offset = 0; } - void setup() override; void dump_config() override; void set_parent(ModbusController *parent) { this->parent_ = parent; } @@ -31,7 +28,7 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusOutput *, float, std::vector &)>; + using write_transform_func_t = std::function(ModbusFloatOutput *, float, std::vector &)>; void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } @@ -44,5 +41,35 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor bool use_write_multiple_; }; +class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem { + public: + ModbusBinaryOutput(uint16_t start_address, uint8_t offset) : output::BinaryOutput(), Component() { + this->register_type = ModbusRegisterType::COIL; + this->start_address = start_address; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = 0; + this->register_count = 1; + this->start_address += offset; + this->offset = 0; + } + void dump_config() override; + + void set_parent(ModbusController *parent) { this->parent_ = parent; } + // Do nothing + void parse_and_publish(const std::vector &data) override{}; + + using write_transform_func_t = std::function(ModbusBinaryOutput *, bool, std::vector &)>; + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + + protected: + void write_state(bool state) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_; + bool use_write_multiple_; +}; + } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index c7c3c419d4..ca8d0be720 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -61,7 +61,7 @@ void ModbusSwitch::write_state(bool state) { } } if (!data.empty()) { - ESP_LOGV(TAG, "Modbus Switch write raw: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Modbus Switch write raw: %s", format_hex_pretty(data).c_str()); cmd = ModbusCommandItem::create_custom_command( this->parent_, data, [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp index 25b79474e8..c90890c88f 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -14,18 +14,18 @@ void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Te void ModbusTextSensor::parse_and_publish(const std::vector &data) { std::ostringstream output; uint8_t max_items = this->response_bytes; + uint8_t index = this->offset; char buffer[4]; - bool add_comma = false; - for (auto b : data) { + while ((max_items != 0) && index < data.size()) { + uint8_t b = data[index]; switch (this->encode_) { case RawEncoding::HEXBYTES: sprintf(buffer, "%02x", b); output << buffer; break; case RawEncoding::COMMA: - sprintf(buffer, add_comma ? ",%d" : "%d", b); + sprintf(buffer, index != this->offset ? ",%d" : "%d", b); output << buffer; - add_comma = true; break; // Anything else no encoding case RawEncoding::NONE: @@ -33,9 +33,8 @@ void ModbusTextSensor::parse_and_publish(const std::vector &data) { output << (char) b; break; } - if (--max_items == 0) { - break; - } + + index++; } auto result = output.str(); diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index d677d54d23..755b0c685c 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -80,7 +80,7 @@ MQTTMessageTrigger = mqtt_ns.class_( "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component ) MQTTJsonMessageTrigger = mqtt_ns.class_( - "MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConstRef) + "MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst) ) MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) @@ -311,7 +311,7 @@ async def to_code(config): for conf in config.get(CONF_ON_JSON_MESSAGE, []): trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS]) - await automation.build_automation(trig, [(cg.JsonObjectConstRef, "x")], conf) + await automation.build_automation(trig, [(cg.JsonObjectConst, "x")], conf) MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( @@ -363,7 +363,7 @@ async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string) cg.add(var.set_topic(template_)) - args_ = args + [(cg.JsonObjectRef, "root")] + args_ = args + [(cg.JsonObject, "root")] lambda_ = await cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void) cg.add(var.set_payload(lambda_)) template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 9795d69304..0852a17cf1 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -74,9 +74,9 @@ class CustomMQTTDevice { * } * * // topic parameter can be remove if not needed: - * // e.g.: void on_json_message(JsonObject &payload) { + * // e.g.: void on_json_message(JsonObject payload) { * - * void on_json_message(const std::string &topic, JsonObject &payload) { + * void on_json_message(const std::string &topic, JsonObject payload) { * // do something with topic and payload * if (payload["number"] == 1) { * digitalWrite(5, HIGH); @@ -93,11 +93,9 @@ class CustomMQTTDevice { * @param qos The Quality of Service to subscribe with. Defaults to 0. */ template - void subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject &), - uint8_t qos = 0); + void subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos = 0); - template - void subscribe_json(const std::string &topic, void (T::*callback)(JsonObject &), uint8_t qos = 0); + template void subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos = 0); /** Publish an MQTT message with the given payload and QoS and retain settings. * @@ -155,7 +153,7 @@ class CustomMQTTDevice { * * ```cpp * void in_some_method() { - * publish("the/topic", [=](JsonObject &root) { + * publish("the/topic", [=](JsonObject root) { * root["the_key"] = "Hello World!"; * }, 0, false); * } @@ -174,7 +172,7 @@ class CustomMQTTDevice { * * ```cpp * void in_some_method() { - * publish("the/topic", [=](JsonObject &root) { + * publish("the/topic", [=](JsonObject root) { * root["the_key"] = "Hello World!"; * }); * } @@ -205,13 +203,13 @@ template void CustomMQTTDevice::subscribe(const std::string &topic, global_mqtt_client->subscribe(topic, f, qos); } template -void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject &), +void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos) { auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); global_mqtt_client->subscribe_json(topic, f, qos); } template -void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject &), uint8_t qos) { +void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos) { auto f = std::bind(callback, (T *) this, std::placeholders::_2); global_mqtt_client->subscribe_json(topic, f, qos); } diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 0a161f89a1..0bf3b751fd 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -29,7 +29,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } } -void MQTTBinarySensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (!this->binary_sensor_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 0efb490367..f6579fcd19 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -22,7 +22,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; void set_is_status(bool status); diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 5f3aaa1dd9..52df63093a 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -30,7 +30,7 @@ void MQTTButtonComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -void MQTTButtonComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { config.state_topic = false; if (!this->button_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); diff --git a/esphome/components/mqtt/mqtt_button.h b/esphome/components/mqtt/mqtt_button.h index 66e4b2609f..42389caecc 100644 --- a/esphome/components/mqtt/mqtt_button.h +++ b/esphome/components/mqtt/mqtt_button.h @@ -23,7 +23,7 @@ class MQTTButtonComponent : public mqtt::MQTTComponent { /// Buttons do not send a state so just return true. bool send_initial_state() override { return true; } - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; protected: /// "button" component type. diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 43c49e9f7f..de25c5b2e3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -1,4 +1,5 @@ #include "mqtt_client.h" +#define USE_MQTT #ifdef USE_MQTT @@ -346,7 +347,7 @@ void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t ca void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) { auto f = [callback](const std::string &topic, const std::string &payload) { - json::parse_json(payload, [topic, callback](JsonObject &root) { callback(topic, root); }); + json::parse_json(payload, [topic, callback](JsonObject root) { callback(topic, root); }); }; MQTTSubscription subscription{ .topic = topic, @@ -416,9 +417,8 @@ bool MQTTClientComponent::publish(const MQTTMessage &message) { } bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) { - size_t len; - const char *message = json::build_json(f, &len); - return this->publish(topic, message, len, qos, retain); + std::string message = json::build_json(f); + return this->publish(topic, message, qos, retain); } /** Check if the message topic matches the given subscription topic diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index d6194da794..a6a7025c6f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -20,7 +20,7 @@ namespace mqtt { * First parameter is the topic, the second one is the payload. */ using mqtt_callback_t = std::function; -using mqtt_json_callback_t = std::function; +using mqtt_json_callback_t = std::function; /// internal struct for MQTT messages. struct MQTTMessage { @@ -306,11 +306,11 @@ class MQTTMessageTrigger : public Trigger, public Component { optional payload_; }; -class MQTTJsonMessageTrigger : public Trigger { +class MQTTJsonMessageTrigger : public Trigger { public: explicit MQTTJsonMessageTrigger(const std::string &topic, uint8_t qos) { global_mqtt_client->subscribe_json( - topic, [this](const std::string &topic, JsonObject &root) { this->trigger(root); }, qos); + topic, [this](const std::string &topic, JsonObject root) { this->trigger(root); }, qos); } }; @@ -338,7 +338,7 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - void set_payload(std::function payload) { this->payload_ = payload; } + void set_payload(std::function payload) { this->payload_ = payload; } void play(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); @@ -349,8 +349,8 @@ template class MQTTPublishJsonAction : public Action { } protected: - void encode_(Ts... x, JsonObject &root) { this->payload_(x..., root); } - std::function payload_; + void encode_(Ts... x, JsonObject root) { this->payload_(x..., root); } + std::function payload_; MQTTClientComponent *parent_; }; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index ebc708f444..f6ef3a5e8f 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -13,7 +13,7 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; -void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { @@ -25,7 +25,7 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC // mode_state_topic root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); // modes - JsonArray &modes = root.createNestedArray(MQTT_MODES); + JsonArray modes = root.createNestedArray(MQTT_MODES); // sort array for nice UI in HA if (traits.supports_mode(CLIMATE_MODE_AUTO)) modes.add("auto"); @@ -83,7 +83,7 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC // fan_mode_state_topic root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); // fan_modes - JsonArray &fan_modes = root.createNestedArray("fan_modes"); + JsonArray fan_modes = root.createNestedArray("fan_modes"); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) fan_modes.add("on"); if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) @@ -112,7 +112,7 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC // swing_mode_state_topic root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); // swing_modes - JsonArray &swing_modes = root.createNestedArray("swing_modes"); + JsonArray swing_modes = root.createNestedArray("swing_modes"); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) swing_modes.add("off"); if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index 40ac4c18c1..ea3e2ab3fa 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -14,7 +14,7 @@ namespace mqtt { class MQTTClimateComponent : public mqtt::MQTTComponent { public: MQTTClimateComponent(climate::Climate *device); - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; std::string component_type() const override; void setup() override; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index bf9f5e34b8..62dbae3bcc 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -63,7 +63,7 @@ bool MQTTComponent::send_discovery_() { return global_mqtt_client->publish_json( this->get_discovery_topic_(discovery_info), - [this](JsonObject &root) { + [this](JsonObject root) { SendDiscoveryConfig config; config.state_topic = true; config.command_topic = true; @@ -127,7 +127,7 @@ bool MQTTComponent::send_discovery_() { } } - JsonObject &device_info = root.createNestedObject(MQTT_DEVICE); + JsonObject device_info = root.createNestedObject(MQTT_DEVICE); device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); device_info[MQTT_DEVICE_NAME] = node_name; device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 657ab7b608..e83523a712 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -70,7 +70,7 @@ class MQTTComponent : public Component { void call_dump_config() override; /// Send discovery info the Home Assistant, override this. - virtual void send_discovery(JsonObject &root, SendDiscoveryConfig &config) = 0; + virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config) = 0; virtual bool send_initial_state() = 0; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 7e42abcd05..e5525bc0f7 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -63,7 +63,7 @@ void MQTTCoverComponent::dump_config() { ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic().c_str()); } } -void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (!this->cover_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 149d46ac85..f3e6053d0b 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -16,7 +16,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { explicit MQTTCoverComponent(cover::Cover *cover); void setup() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; MQTT_COMPONENT_CUSTOM_TOPIC(position, command) MQTT_COMPONENT_CUSTOM_TOPIC(position, state) diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index d58e3abc88..0f2eb6535f 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -120,7 +120,7 @@ void MQTTFanComponent::dump_config() { bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } -void MQTTFanComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->state_->get_traits().supports_oscillation()) { root[MQTT_OSCILLATION_COMMAND_TOPIC] = this->get_oscillation_command_topic(); root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic(); diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index a160d5366b..9d15a6cd0e 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -22,7 +22,7 @@ class MQTTFanComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(speed, command) MQTT_COMPONENT_CUSTOM_TOPIC(speed, state) - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 54204a9e7f..ee1cc36af7 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -18,7 +18,7 @@ std::string MQTTJSONLightComponent::component_type() const { return "light"; } const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; } void MQTTJSONLightComponent::setup() { - this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject &root) { + this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { LightCall call = this->state_->make_call(); LightJSONSchema::parse_json(*this->state_, call, root); call.perform(); @@ -32,16 +32,16 @@ MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponen bool MQTTJSONLightComponent::publish_state_() { return this->publish_json(this->get_state_topic_(), - [this](JsonObject &root) { LightJSONSchema::dump_json(*this->state_, root); }); + [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } -void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { root["schema"] = "json"; auto traits = this->state_->get_traits(); root[MQTT_COLOR_MODE] = true; - JsonArray &color_modes = root.createNestedArray("supported_color_modes"); + JsonArray color_modes = root.createNestedArray("supported_color_modes"); if (traits.supports_color_mode(ColorMode::ON_OFF)) color_modes.add("onoff"); if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) @@ -66,7 +66,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover if (this->state_->supports_effects()) { root["effect"] = true; - JsonArray &effect_list = root.createNestedArray(MQTT_EFFECT_LIST); + JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST); for (auto *effect : this->state_->get_effects()) effect_list.add(effect->get_name()); effect_list.add("None"); diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 192cba39b6..3d1e770d4d 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -21,7 +21,7 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 18e3a61417..73d37f7cd3 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -37,7 +37,7 @@ void MQTTNumberComponent::dump_config() { std::string MQTTNumberComponent::component_type() const { return "number"; } const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_; } -void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = number_->traits; // https://www.home-assistant.io/integrations/number.mqtt/ root[MQTT_MIN] = traits.get_min_value(); diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h index 66622d7c29..10500c8333 100644 --- a/esphome/components/mqtt/mqtt_number.h +++ b/esphome/components/mqtt/mqtt_number.h @@ -25,7 +25,7 @@ class MQTTNumberComponent : public mqtt::MQTTComponent { void setup() override; void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b8371de00e..cb4c9c9052 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -32,10 +32,10 @@ void MQTTSelectComponent::dump_config() { std::string MQTTSelectComponent::component_type() const { return "select"; } const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_; } -void MQTTSelectComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = select_->traits; // https://www.home-assistant.io/integrations/select.mqtt/ - JsonArray &options = root.createNestedArray(MQTT_OPTIONS); + JsonArray options = root.createNestedArray(MQTT_OPTIONS); for (const auto &option : traits.get_options()) options.add(option); diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h index d77d0cf513..e0d8ac2417 100644 --- a/esphome/components/mqtt/mqtt_select.h +++ b/esphome/components/mqtt/mqtt_select.h @@ -25,7 +25,7 @@ class MQTTSelectComponent : public mqtt::MQTTComponent { void setup() override; void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index dd6423e8f3..303aa0e753 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -42,7 +42,7 @@ uint32_t MQTTSensorComponent::get_expire_after() const { void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; } void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } -void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (!this->sensor_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 22609fdfef..adc201736a 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -27,7 +27,7 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { /// Disable Home Assistant value expiry. void disable_expire_after(); - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index edaa6e7859..2e91f8e502 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -44,7 +44,7 @@ void MQTTSwitchComponent::dump_config() { std::string MQTTSwitchComponent::component_type() const { return "switch"; } const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } -void MQTTSwitchComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->switch_->assumed_state()) root[MQTT_OPTIMISTIC] = true; } diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index a0a7a23220..c4d3f7164c 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -20,7 +20,7 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { void setup() override; void dump_config() override; - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 7b89915649..010364e221 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -12,7 +12,7 @@ static const char *const TAG = "mqtt.text_sensor"; using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : MQTTComponent(), sensor_(sensor) {} -void MQTTTextSensor::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { +void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index 83743245cc..fe53a6fefd 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -15,7 +15,7 @@ class MQTTTextSensor : public mqtt::MQTTComponent { public: explicit MQTTTextSensor(text_sensor::TextSensor *sensor); - void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; void setup() override; diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 494765db4d..fcb3885db9 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -329,6 +329,7 @@ void Nextion::process_nextion_commands_() { break; case 0x02: // invalid Component ID or name was used + ESP_LOGW(TAG, "Nextion reported component ID or name invalid!"); this->remove_from_q_(); break; case 0x03: // invalid Page ID or name was used @@ -387,6 +388,7 @@ void Nextion::process_nextion_commands_() { } break; case 0x1A: // variable name invalid + ESP_LOGW(TAG, "Nextion reported variable name invalid!"); this->remove_from_q_(); break; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 931b934ba2..f83aafc595 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -171,7 +171,7 @@ void Nextion::set_component_coordinates(const char *component, int x, int y) { // Drawing void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->add_no_result_to_queue_with_printf_("display_picture", "pic %d %d %d", x_start, y_start, picture_id); + this->add_no_result_to_queue_with_printf_("display_picture", "pic %d, %d, %d", x_start, y_start, picture_id); } void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index b16f2fe7eb..1b60034bd1 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -8,6 +8,10 @@ #include "esphome/core/log.h" #include "esphome/components/network/util.h" +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace nextion { static const char *const TAG = "nextion_upload"; @@ -158,12 +162,8 @@ void Nextion::upload_tft() { if (!begin_status) { this->is_updating_ = false; ESP_LOGD(TAG, "connection failed"); -#ifdef USE_ESP32 - if (psramFound()) - free(this->transfer_buffer_); // NOLINT - else -#endif - delete this->transfer_buffer_; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); return; } else { ESP_LOGD(TAG, "Connected"); @@ -252,7 +252,7 @@ void Nextion::upload_tft() { // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 #ifdef USE_ESP32 uint32_t chunk_size = 8192; - if (psramFound()) { + if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) { chunk_size = this->content_length_; } else { if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand @@ -269,32 +269,18 @@ void Nextion::upload_tft() { #endif if (this->transfer_buffer_ == nullptr) { -#ifdef USE_ESP32 - if (psramFound()) { - ESP_LOGD(TAG, "Allocating PSRAM buffer size %d, Free PSRAM size is %u", chunk_size, ESP.getFreePsram()); - this->transfer_buffer_ = (uint8_t *) ps_malloc(chunk_size); - if (this->transfer_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate buffer size %d!", chunk_size); - this->upload_end_(); - } - } else { -#endif - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; - if (this->transfer_buffer_ == nullptr) { // Try a smaller size - ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); - chunk_size = 4096; - ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - this->transfer_buffer_ = new uint8_t[chunk_size]; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + this->transfer_buffer_ = allocator.allocate(chunk_size); + if (this->transfer_buffer_ == nullptr) { // Try a smaller size + ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); + this->transfer_buffer_ = allocator.allocate(chunk_size); - if (!this->transfer_buffer_) - this->upload_end_(); -#ifdef USE_ESP32 - } -#endif + if (!this->transfer_buffer_) + this->upload_end_(); } this->transfer_buffer_size_ = chunk_size; diff --git a/esphome/components/output/switch/__init__.py b/esphome/components/output/switch/__init__.py index 11d073d28c..46135d117e 100644 --- a/esphome/components/output/switch/__init__.py +++ b/esphome/components/output/switch/__init__.py @@ -1,15 +1,28 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output, switch -from esphome.const import CONF_ID, CONF_OUTPUT +from esphome.const import CONF_ID, CONF_OUTPUT, CONF_RESTORE_MODE from .. import output_ns OutputSwitch = output_ns.class_("OutputSwitch", switch.Switch, cg.Component) +OutputSwitchRestoreMode = output_ns.enum("OutputSwitchRestoreMode") +RESTORE_MODES = { + "RESTORE_DEFAULT_OFF": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_DEFAULT_OFF, + "RESTORE_DEFAULT_ON": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_DEFAULT_ON, + "ALWAYS_OFF": OutputSwitchRestoreMode.OUTPUT_SWITCH_ALWAYS_OFF, + "ALWAYS_ON": OutputSwitchRestoreMode.OUTPUT_SWITCH_ALWAYS_ON, + "RESTORE_INVERTED_DEFAULT_OFF": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + "RESTORE_INVERTED_DEFAULT_ON": OutputSwitchRestoreMode.OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_ON, +} + CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(OutputSwitch), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -21,3 +34,5 @@ async def to_code(config): output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/output/switch/output_switch.cpp b/esphome/components/output/switch/output_switch.cpp index 8db45f3a2b..3691896cbe 100644 --- a/esphome/components/output/switch/output_switch.cpp +++ b/esphome/components/output/switch/output_switch.cpp @@ -8,15 +8,32 @@ static const char *const TAG = "output.switch"; void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); } void OutputSwitch::setup() { - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; - - if (*restored) { - this->turn_on(); - } else { - this->turn_off(); + bool initial_state = false; + switch (this->restore_mode_) { + case OUTPUT_SWITCH_RESTORE_DEFAULT_OFF: + initial_state = this->get_initial_state().value_or(false); + break; + case OUTPUT_SWITCH_RESTORE_DEFAULT_ON: + initial_state = this->get_initial_state().value_or(true); + break; + case OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_OFF: + initial_state = !this->get_initial_state().value_or(true); + break; + case OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_ON: + initial_state = !this->get_initial_state().value_or(false); + break; + case OUTPUT_SWITCH_ALWAYS_OFF: + initial_state = false; + break; + case OUTPUT_SWITCH_ALWAYS_ON: + initial_state = true; + break; } + + if (initial_state) + this->turn_on(); + else + this->turn_off(); } void OutputSwitch::write_state(bool state) { if (state) { diff --git a/esphome/components/output/switch/output_switch.h b/esphome/components/output/switch/output_switch.h index a184a342fe..fc2c110662 100644 --- a/esphome/components/output/switch/output_switch.h +++ b/esphome/components/output/switch/output_switch.h @@ -7,10 +7,21 @@ namespace esphome { namespace output { +enum OutputSwitchRestoreMode { + OUTPUT_SWITCH_RESTORE_DEFAULT_OFF, + OUTPUT_SWITCH_RESTORE_DEFAULT_ON, + OUTPUT_SWITCH_ALWAYS_OFF, + OUTPUT_SWITCH_ALWAYS_ON, + OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, + OUTPUT_SWITCH_RESTORE_INVERTED_DEFAULT_ON, +}; + class OutputSwitch : public switch_::Switch, public Component { public: void set_output(BinaryOutput *output) { output_ = output; } + void set_restore_mode(OutputSwitchRestoreMode restore_mode) { restore_mode_ = restore_mode; } + void setup() override; float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } void dump_config() override; @@ -19,6 +30,7 @@ class OutputSwitch : public switch_::Switch, public Component { void write_state(bool state) override; output::BinaryOutput *output_; + OutputSwitchRestoreMode restore_mode_; }; } // namespace output diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index ec32e45b3d..be58f265b9 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -26,7 +26,7 @@ bool PN532Spi::write_data(const std::vector &data) { delay(2); // First byte, communication mode: Write data this->write_byte(0x01); - ESP_LOGV(TAG, "Writing data: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty(data).c_str()); this->write_array(data.data(), data.size()); this->disable(); @@ -65,7 +65,7 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { this->read_array(data.data(), len); this->disable(); data.insert(data.begin(), 0x01); - ESP_LOGV(TAG, "Read data: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Read data: %s", format_hex_pretty(data).c_str()); return true; } @@ -97,7 +97,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { std::vector header(7); this->read_array(header.data(), 7); - ESP_LOGV(TAG, "Header data: %s", hexencode(header).c_str()); + ESP_LOGV(TAG, "Header data: %s", format_hex_pretty(header).c_str()); if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { // invalid packet @@ -127,7 +127,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { this->read_array(data.data(), len + 1); this->disable(); - ESP_LOGV(TAG, "Response data: %s", hexencode(data).c_str()); + ESP_LOGV(TAG, "Response data: %s", format_hex_pretty(data).c_str()); uint8_t checksum = header[5] + header[6]; // TFI + Command response code for (int i = 0; i < len - 1; i++) { diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index fa7b4fe132..618c866d5b 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace prometheus { void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { - AsyncResponseStream *stream = req->beginResponseStream("text/plain"); + AsyncResponseStream *stream = req->beginResponseStream("text/plain; version=0.0.4; charset=utf-8"); #ifdef USE_SENSOR this->sensor_type_(stream); diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py new file mode 100644 index 0000000000..ac6d034514 --- /dev/null +++ b/esphome/components/psram/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.core import CORE +from esphome.const import ( + CONF_ID, +) + +CODEOWNERS = ["@esphome/core"] + +psram_ns = cg.esphome_ns.namespace("psram") +PsramComponent = psram_ns.class_("PsramComponent", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(PsramComponent)}), cv.only_on_esp32 +) + + +async def to_code(config): + if CORE.using_arduino: + cg.add_build_flag("-DBOARD_HAS_PSRAM") + + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ESP32_SPIRAM_SUPPORT", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp new file mode 100644 index 0000000000..8325709632 --- /dev/null +++ b/esphome/components/psram/psram.cpp @@ -0,0 +1,32 @@ +#include "psram.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +#include +#include + +namespace esphome { +namespace psram { + +static const char *const TAG = "psram"; + +void PsramComponent::dump_config() { + // Technically this can be false if the PSRAM is full, but heap_caps_get_total_size() isn't always available, and it's + // very unlikely for the PSRAM to be full. + bool available = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0; + + ESP_LOGCONFIG(TAG, "PSRAM:"); + ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) + if (available) { + ESP_LOGCONFIG(TAG, " Size: %d MB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024 / 1024); + } +#endif +} + +} // namespace psram +} // namespace esphome + +#endif diff --git a/esphome/components/psram/psram.h b/esphome/components/psram/psram.h new file mode 100644 index 0000000000..8c891feee9 --- /dev/null +++ b/esphome/components/psram/psram.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" + +namespace esphome { +namespace psram { + +class PsramComponent : public Component { + void dump_config() override; +}; + +} // namespace psram +} // namespace esphome + +#endif diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index d9f198f4fc..5232ebc427 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "pulse_counter"; const char *const EDGE_MODE_TO_STRING[] = {"DISABLE", "INCREMENT", "DECREMENT"}; -#ifdef USE_ESP8266 +#ifndef HAS_PCNT void IRAM_ATTR PulseCounterStorage::gpio_intr(PulseCounterStorage *arg) { const uint32_t now = micros(); const bool discard = now - arg->last_pulse < arg->filter_us; @@ -43,7 +43,7 @@ pulse_counter_t PulseCounterStorage::read_raw_value() { } #endif -#ifdef USE_ESP32 +#ifdef HAS_PCNT bool PulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0; this->pin = pin; @@ -96,7 +96,7 @@ bool PulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint16_t filter_val = std::min(this->filter_us * 80u, 1023u); + uint16_t filter_val = std::min(static_cast(this->filter_us * 80u), 1023u); ESP_LOGCONFIG(TAG, " Filter Value: %uus (val=%u)", this->filter_us, filter_val); error = pcnt_set_filter_value(this->pcnt_unit, filter_val); if (error != ESP_OK) { diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 9ed2159ae3..86c387d52a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -4,8 +4,9 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include +#define HAS_PCNT #endif namespace esphome { @@ -17,10 +18,9 @@ enum PulseCounterCountMode { PULSE_COUNTER_DECREMENT, }; -#ifdef USE_ESP32 +#ifdef HAS_PCNT using pulse_counter_t = int16_t; -#endif -#ifdef USE_ESP8266 +#else using pulse_counter_t = int32_t; #endif @@ -30,16 +30,15 @@ struct PulseCounterStorage { static void gpio_intr(PulseCounterStorage *arg); -#ifdef USE_ESP8266 +#ifndef HAS_PCNT volatile pulse_counter_t counter{0}; volatile uint32_t last_pulse{0}; #endif InternalGPIOPin *pin; -#ifdef USE_ESP32 +#ifdef HAS_PCNT pcnt_unit_t pcnt_unit; -#endif -#ifdef USE_ESP8266 +#else ISRInternalGPIOPin isr_pin; #endif PulseCounterCountMode rising_edge_mode{PULSE_COUNTER_INCREMENT}; diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index b1a9607304..c3738d1852 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -7,6 +7,7 @@ namespace pzemac { static const char *const TAG = "pzemac"; static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers void PZEMAC::on_modbus_data(const std::vector &data) { @@ -73,5 +74,12 @@ void PZEMAC::dump_config() { LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); } +void PZEMAC::reset_energy_() { + std::vector cmd; + cmd.push_back(this->address_); + cmd.push_back(PZEM_CMD_RESET_ENERGY); + this->send_raw(cmd); +} + } // namespace pzemac } // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 07f661535f..e9f76972a3 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" @@ -7,6 +8,8 @@ namespace esphome { namespace pzemac { +template class ResetEnergyAction; + class PZEMAC : public PollingComponent, public modbus::ModbusDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } @@ -23,12 +26,25 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { void dump_config() override; protected: + template friend class ResetEnergyAction; sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; sensor::Sensor *energy_sensor_; sensor::Sensor *frequency_sensor_; sensor::Sensor *power_factor_sensor_; + + void reset_energy_(); +}; + +template class ResetEnergyAction : public Action { + public: + ResetEnergyAction(PZEMAC *pzemac) : pzemac_(pzemac) {} + + void play(Ts... x) override { this->pzemac_->reset_energy_(); } + + protected: + PZEMAC *pzemac_; }; } // namespace pzemac diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index b6697e3d19..ab7dd3e202 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -1,5 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id from esphome.components import sensor, modbus from esphome.const import ( CONF_CURRENT, @@ -29,6 +31,9 @@ AUTO_LOAD = ["modbus"] pzemac_ns = cg.esphome_ns.namespace("pzemac") PZEMAC = pzemac_ns.class_("PZEMAC", cg.PollingComponent, modbus.ModbusDevice) +# Actions +ResetEnergyAction = pzemac_ns.class_("ResetEnergyAction", automation.Action) + CONFIG_SCHEMA = ( cv.Schema( { @@ -75,6 +80,20 @@ CONFIG_SCHEMA = ( ) +@automation.register_action( + "pzemac.reset_energy", + ResetEnergyAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PZEMAC), + } + ), +) +async def reset_energy_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 914ce42efe..72a91a99dd 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -27,6 +27,7 @@ from esphome.const import ( CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2, + CONF_LEVEL, ) from esphome.core import coroutine from esphome.jsonschema import jschema_extractor @@ -234,6 +235,45 @@ async def build_dumpers(config): return dumpers +# Coolix +( + CoolixData, + CoolixBinarySensor, + CoolixTrigger, + CoolixAction, + CoolixDumper, +) = declare_protocol("Coolix") +COOLIX_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) + + +@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SCHEMA) +def coolix_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CoolixData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("coolix", CoolixTrigger, CoolixData) +def coolix_trigger(var, config): + pass + + +@register_dumper("coolix", CoolixDumper) +def coolix_dumper(var, config): + pass + + +@register_action("coolix", CoolixAction, COOLIX_SCHEMA) +async def coolix_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + cg.add(var.set_data(template_)) + + # Dish DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( "Dish" @@ -1124,6 +1164,58 @@ async def panasonic_action(var, config, args): cg.add(var.set_command(template_)) +# Nexa +NexaData, NexaBinarySensor, NexaTrigger, NexaAction, NexaDumper = declare_protocol( + "Nexa" +) +NEXA_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint32_t, + cv.Required(CONF_GROUP): cv.hex_uint8_t, + cv.Required(CONF_STATE): cv.hex_uint8_t, + cv.Required(CONF_CHANNEL): cv.hex_uint8_t, + cv.Required(CONF_LEVEL): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("nexa", NexaBinarySensor, NEXA_SCHEMA) +def nexa_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + NexaData, + ("device", config[CONF_DEVICE]), + ("group", config[CONF_GROUP]), + ("state", config[CONF_STATE]), + ("channel", config[CONF_CHANNEL]), + ("level", config[CONF_LEVEL]), + ) + ) + ) + + +@register_trigger("nexa", NexaTrigger, NexaData) +def nexa_trigger(var, config): + pass + + +@register_dumper("nexa", NexaDumper) +def nexa_dumper(var, config): + pass + + +@register_action("nexa", NexaAction, NEXA_SCHEMA) +def nexa_action(var, config, args): + cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint32)))) + cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8)))) + cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, cg.uint8)))) + cg.add( + var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + ) + cg.add(var.set_level((yield cg.templatable(config[CONF_LEVEL], args, cg.uint8)))) + + # Midea MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( "Midea" diff --git a/esphome/components/remote_base/coolix_protocol.cpp b/esphome/components/remote_base/coolix_protocol.cpp new file mode 100644 index 0000000000..3e6e7e185a --- /dev/null +++ b/esphome/components/remote_base/coolix_protocol.cpp @@ -0,0 +1,84 @@ +#include "coolix_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.coolix"; + +static const int32_t TICK_US = 560; +static const int32_t HEADER_MARK_US = 8 * TICK_US; +static const int32_t HEADER_SPACE_US = 8 * TICK_US; +static const int32_t BIT_MARK_US = 1 * TICK_US; +static const int32_t BIT_ONE_SPACE_US = 3 * TICK_US; +static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; +static const int32_t FOOTER_MARK_US = 1 * TICK_US; +static const int32_t FOOTER_SPACE_US = 10 * TICK_US; + +static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { + // Break data into bytes, starting at the Most Significant + // Byte. Each byte then being sent normal, then followed inverted. + for (unsigned shift = 16;; shift -= 8) { + // Grab a bytes worth of data. + const uint8_t byte = src >> shift; + // Normal + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); + // Inverted + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (byte & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); + // Data end + if (shift == 0) + break; + } +} + +void CoolixProtocol::encode(RemoteTransmitData *dst, const CoolixData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 2 * 48 + 2 + 2 + 2 * 48 + 1); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + encode_data(dst, data); + dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + encode_data(dst, data); + dst->mark(FOOTER_MARK_US); +} + +static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { + uint32_t data = 0; + for (unsigned n = 3;; data <<= 8) { + // Read byte + for (uint32_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_mark(BIT_MARK_US)) + return false; + if (src.expect_space(BIT_ONE_SPACE_US)) + data |= mask; + else if (!src.expect_space(BIT_ZERO_SPACE_US)) + return false; + } + // Check for inverse byte + for (uint32_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_item(BIT_MARK_US, (data & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) + return false; + } + // Checking the end of reading + if (--n == 0) { + dst = data; + return true; + } + } +} + +optional CoolixProtocol::decode(RemoteReceiveData data) { + CoolixData first, second; + if (data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(data, first) && + data.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && + decode_data(data, second) && data.expect_mark(FOOTER_MARK_US) && first == second) + return first; + return {}; +} + +void CoolixProtocol::dump(const CoolixData &data) { ESP_LOGD(TAG, "Received Coolix: 0x%06X", data); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h new file mode 100644 index 0000000000..9ce3eabb0e --- /dev/null +++ b/esphome/components/remote_base/coolix_protocol.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +using CoolixData = uint32_t; + +class CoolixProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const CoolixData &data) override; + optional decode(RemoteReceiveData data) override; + void dump(const CoolixData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Coolix) + +template class CoolixAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(CoolixData, data) + void encode(RemoteTransmitData *dst, Ts... x) override { + CoolixData data = this->data_.value(x...); + CoolixProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp index baf64f246f..bf67429001 100644 --- a/esphome/components/remote_base/midea_protocol.cpp +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -6,89 +6,63 @@ namespace remote_base { static const char *const TAG = "remote.midea"; +static const int32_t TICK_US = 560; +static const int32_t HEADER_MARK_US = 8 * TICK_US; +static const int32_t HEADER_SPACE_US = 8 * TICK_US; +static const int32_t BIT_MARK_US = 1 * TICK_US; +static const int32_t BIT_ONE_SPACE_US = 3 * TICK_US; +static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; +static const int32_t FOOTER_MARK_US = 1 * TICK_US; +static const int32_t FOOTER_SPACE_US = 10 * TICK_US; + uint8_t MideaData::calc_cs_() const { uint8_t cs = 0; - for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it) - cs -= reverse_bits_8(*it); - return reverse_bits_8(cs); + for (uint8_t idx = 0; idx < OFFSET_CS; idx++) + cs -= reverse_bits(this->data_[idx]); + return reverse_bits(cs); } -bool MideaData::check_compliment(const MideaData &rhs) const { - const uint8_t *it0 = rhs.data(); - for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) { - if (*it0 != ~(*it1)) - return false; - } - return true; +bool MideaData::is_compliment(const MideaData &rhs) const { + return std::equal(this->data_.begin(), this->data_.end(), rhs.data_.begin(), + [](const uint8_t &a, const uint8_t &b) { return a + b == 255; }); } -void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) { - for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) { - const uint8_t data = compliment ? ~(*it) : *it; - for (uint8_t mask = 128; mask; mask >>= 1) { - if (data & mask) - one(dst); - else - zero(dst); - } - } -} - -void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) { +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &src) { dst->set_carrier_frequency(38000); - dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2); - MideaProtocol::header(dst); - MideaProtocol::data(dst, data); - MideaProtocol::footer(dst); - MideaProtocol::header(dst); - MideaProtocol::data(dst, data, true); - MideaProtocol::footer(dst); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 1); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + for (unsigned idx = 0; idx < 6; idx++) + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (src[idx] & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); + dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + for (unsigned idx = 0; idx < 6; idx++) + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (src[idx] & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); + dst->mark(FOOTER_MARK_US); } -bool MideaProtocol::expect_one(RemoteReceiveData &src) { - if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_zero(RemoteReceiveData &src) { - if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_header(RemoteReceiveData &src) { - if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_footer(RemoteReceiveData &src) { - if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) { - for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) { - for (uint8_t mask = 128; mask; mask >>= 1) { - if (MideaProtocol::expect_one(src)) - *dst |= mask; - else if (!MideaProtocol::expect_zero(src)) +static bool decode_data(RemoteReceiveData &src, MideaData &dst) { + for (unsigned idx = 0; idx < 6; idx++) { + uint8_t data = 0; + for (uint8_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_mark(BIT_MARK_US)) + return false; + if (src.expect_space(BIT_ONE_SPACE_US)) + data |= mask; + else if (!src.expect_space(BIT_ZERO_SPACE_US)) return false; } + dst[idx] = data; } return true; } optional MideaProtocol::decode(RemoteReceiveData src) { MideaData out, inv; - if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) && - out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv)) + if (src.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(src, out) && out.is_valid() && + src.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && src.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && + decode_data(src, inv) && src.expect_mark(FOOTER_MARK_US) && out.is_compliment(inv)) return out; return {}; } diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 35ea23acfb..135a93b36d 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "remote_base.h" @@ -9,70 +10,61 @@ namespace remote_base { class MideaData { public: - // Make zero-filled - MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make default + MideaData() {} // Make from initializer_list - MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + MideaData(std::initializer_list data) { + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); + } // Make from vector MideaData(const std::vector &data) { - memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Default copy constructor MideaData(const MideaData &) = default; - uint8_t *data() { return this->data_; } - const uint8_t *data() const { return this->data_; } - uint8_t size() const { return sizeof(this->data_); } + uint8_t *data() { return this->data_.data(); } + const uint8_t *data() const { return this->data_.data(); } + uint8_t size() const { return this->data_.size(); } bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } - bool check_compliment(const MideaData &rhs) const; - std::string to_string() const { return hexencode(*this); } + bool is_compliment(const MideaData &rhs) const; + std::string to_string() const { return format_hex_pretty(this->data_.data(), this->data_.size()); } // compare only 40-bits - bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + bool operator==(const MideaData &rhs) const { + return std::equal(this->data_.begin(), this->data_.begin() + OFFSET_CS, rhs.data_.begin()); + } enum MideaDataType : uint8_t { - MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_CONTROL = 0xA1, MIDEA_TYPE_SPECIAL = 0xA2, MIDEA_TYPE_FOLLOW_ME = 0xA4, }; MideaDataType type() const { return static_cast(this->data_[0]); } template T to() const { return T(*this); } + uint8_t &operator[](size_t idx) { return this->data_[idx]; } + const uint8_t &operator[](size_t idx) const { return this->data_[idx]; } protected: - void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) { - data_[offset] &= ~(val_mask << shift); - data_[offset] |= (val << shift); + uint8_t get_value_(uint8_t idx, uint8_t mask = 255, uint8_t shift = 0) const { + return (this->data_[idx] >> shift) & mask; } + void set_value_(uint8_t idx, uint8_t value, uint8_t mask = 255, uint8_t shift = 0) { + this->data_[idx] &= ~(mask << shift); + this->data_[idx] |= (value << shift); + } + void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } static const uint8_t OFFSET_CS = 5; // 48-bits data - uint8_t data_[6]; + std::array data_; // Calculate checksum uint8_t calc_cs_() const; }; class MideaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MideaData &data) override; + void encode(RemoteTransmitData *dst, const MideaData &src) override; optional decode(RemoteReceiveData src) override; void dump(const MideaData &data) override; - - protected: - static const int32_t TICK_US = 560; - static const int32_t HEADER_HIGH_US = 8 * TICK_US; - static const int32_t HEADER_LOW_US = 8 * TICK_US; - static const int32_t BIT_HIGH_US = 1 * TICK_US; - static const int32_t BIT_ONE_LOW_US = 3 * TICK_US; - static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US; - static const int32_t MIN_GAP_US = 10 * TICK_US; - static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); } - static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } - static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); } - static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); } - static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false); - static bool expect_one(RemoteReceiveData &src); - static bool expect_zero(RemoteReceiveData &src); - static bool expect_header(RemoteReceiveData &src); - static bool expect_footer(RemoteReceiveData &src); - static bool expect_data(RemoteReceiveData &src, MideaData &out); }; class MideaBinarySensor : public RemoteReceiverBinarySensorBase { diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp new file mode 100644 index 0000000000..814b46135a --- /dev/null +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -0,0 +1,235 @@ +#include "nexa_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.nexa"; + +static const uint8_t NBITS = 32; +static const uint32_t HEADER_HIGH_US = 319; +static const uint32_t HEADER_LOW_US = 2610; +static const uint32_t BIT_HIGH_US = 319; +static const uint32_t BIT_ONE_LOW_US = 1000; +static const uint32_t BIT_ZERO_LOW_US = 140; + +static const uint32_t TX_HEADER_HIGH_US = 250; +static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static const uint32_t TX_BIT_HIGH_US = 250; +static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; + +void NexaProtocol::one(RemoteTransmitData *dst) const { + // '1' => '10' + dst->item(TX_BIT_HIGH_US, TX_BIT_ONE_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); +} + +void NexaProtocol::zero(RemoteTransmitData *dst) const { + // '0' => '01' + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ONE_LOW_US); +} + +void NexaProtocol::sync(RemoteTransmitData *dst) const { dst->item(TX_HEADER_HIGH_US, TX_HEADER_LOW_US); } + +void NexaProtocol::encode(RemoteTransmitData *dst, const NexaData &data) { + dst->set_carrier_frequency(0); + + // Send SYNC + this->sync(dst); + + // Device (26 bits) + for (int16_t i = 26 - 1; i >= 0; i--) { + if (data.device & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + + // Group (1 bit) + if (data.group != 0) + this->one(dst); + else + this->zero(dst); + + // State (1 bit) + if (data.state == 2) { + // Special case for dimmers...send 00 as state + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + } else if (data.state == 1) + this->one(dst); + else + this->zero(dst); + + // Channel (4 bits) + for (int16_t i = 4 - 1; i >= 0; i--) { + if (data.channel & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + + // Level (4 bits) + if (data.state == 2) { + for (int16_t i = 4 - 1; i >= 0; i--) { + if (data.level & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + } + + // Send finishing Zero + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); +} + +optional NexaProtocol::decode(RemoteReceiveData src) { + NexaData out{ + .device = 0, + .group = 0, + .state = 0, + .channel = 0, + .level = 0, + }; + + // From: http://tech.jolowe.se/home-automation-rf-protocols/ + // New data: http://tech.jolowe.se/old-home-automation-rf-protocols/ + /* + + SHHHH HHHH HHHH HHHH HHHH HHHH HHGO EE BB DDDD 0 P + + S = Sync bit. + H = The first 26 bits are transmitter unique codes, and it is this code that the reciever "learns" to recognize. + G = Group code, set to one for the whole group. + O = On/Off bit. Set to 1 for on, 0 for off. + E = Unit to be turned on or off. The code is inverted, i.e. '11' equals 1, '00' equals 4. + B = Button code. The code is inverted, i.e. '11' equals 1, '00' equals 4. + D = Dim level bits. + 0 = packet always ends with a zero. + P = Pause, a 10 ms pause in between re-send. + + Update: First of all the '1' and '0' bit seems to be reversed (and be the same as Jula I protocol below), i.e. + + */ + + // Require a SYNC pulse + long gap + if (!src.expect_pulse_with_gap(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + // Device + for (uint8_t i = 0; i < 26; i++) { + out.device <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.device |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.device |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // GROUP + for (uint8_t i = 0; i < 1; i++) { + out.group <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.group |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.group |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // STATE + for (uint8_t i = 0; i < 1; i++) { + out.state <<= 1UL; + + // Special treatment as we should handle 01, 10 and 00 + // We need to care for the advance made in the expect functions + // hence take them one at a time so that we do not get out of sync + // in decoding + + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // Starts with '1' + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // '10' => 1 + out.state |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // '11' => NOT OK + // This case is here to make sure we advance through the correct index + // This should not happen...failed command + return {}; + } + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // Starts with '0' + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // '01' => 0 + out.state |= 0x00; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // '00' => Special case for dimmer! => 2 + out.state |= 0x02; + } + } + } + + // CHANNEL (EE and BB bits) + for (uint8_t i = 0; i < 4; i++) { + out.channel <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.channel |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.channel |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // Optional to transmit LEVEL data (8 bits more) + if (int32_t(src.get_index() + 8) >= src.size()) { + return out; + } + + // LEVEL + for (uint8_t i = 0; i < 4; i++) { + out.level <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.level |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.level |= 0x00; + } else { + // This should not happen...failed command + break; + } + } + + return out; +} + +void NexaProtocol::dump(const NexaData &data) { + ESP_LOGD(TAG, "Received NEXA: device=0x%04X group=%d state=%d channel=%d level=%d", data.device, data.group, + data.state, data.channel, data.level); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h new file mode 100644 index 0000000000..f1ce380780 --- /dev/null +++ b/esphome/components/remote_base/nexa_protocol.h @@ -0,0 +1,52 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct NexaData { + uint32_t device; + uint8_t group; + uint8_t state; + uint8_t channel; + uint8_t level; + bool operator==(const NexaData &rhs) const { + return device == rhs.device && group == rhs.group && state == rhs.state && channel == rhs.channel && + level == rhs.level; + } +}; + +class NexaProtocol : public RemoteProtocol { + public: + void one(RemoteTransmitData *dst) const; + void zero(RemoteTransmitData *dst) const; + void sync(RemoteTransmitData *dst) const; + + void encode(RemoteTransmitData *dst, const NexaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const NexaData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Nexa) + +template class NexaAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, device) + TEMPLATABLE_VALUE(uint8_t, group) + TEMPLATABLE_VALUE(uint8_t, state) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, level) + void encode(RemoteTransmitData *dst, Ts... x) override { + NexaData data{}; + data.device = this->device_.value(x...); + data.group = this->group_.value(x...); + data.state = this->state_.value(x...); + data.channel = this->channel_.value(x...); + data.level = this->level_.value(x...); + NexaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index dd6f7c3482..e1af41274e 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -116,6 +116,16 @@ class RemoteReceiveData { return false; } + bool expect_pulse_with_gap(uint32_t mark, uint32_t space) { + if (this->peek_mark(mark, 0) && this->peek_space_at_least(space, 1)) { + this->advance(2); + return true; + } + return false; + } + + uint32_t get_index() { return index_; } + void reset() { this->index_ = 0; } int32_t pos(uint32_t index) const { return (*this->data_)[index]; } diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index f150d6e086..ab884bfee4 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -CODEOWNERS = ["@paulmonigatti"] +CODEOWNERS = ["@paulmonigatti", "@jsuanet"] safe_mode_ns = cg.esphome_ns.namespace("safe_mode") diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py new file mode 100644 index 0000000000..2cd8892afb --- /dev/null +++ b/esphome/components/safe_mode/button/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.components.ota import OTAComponent +from esphome.const import ( + CONF_ID, + CONF_OTA, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) + +DEPENDENCIES = ["ota"] + +safe_mode_ns = cg.esphome_ns.namespace("safe_mode") +SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component) + +CONFIG_SCHEMA = ( + button.button_schema( + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ) + .extend({cv.GenerateID(): cv.declare_id(SafeModeButton)}) + .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) + + ota = await cg.get_variable(config[CONF_OTA]) + cg.add(var.set_ota(ota)) diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp new file mode 100644 index 0000000000..2b8654de46 --- /dev/null +++ b/esphome/components/safe_mode/button/safe_mode_button.cpp @@ -0,0 +1,25 @@ +#include "safe_mode_button.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace safe_mode { + +static const char *const TAG = "safe_mode.button"; + +void SafeModeButton::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; } + +void SafeModeButton::press_action() { + ESP_LOGI(TAG, "Restarting device in safe mode..."); + this->ota_->set_safe_mode_pending(true); + + // Let MQTT settle a bit + delay(100); // NOLINT + App.safe_reboot(); +} + +void SafeModeButton::dump_config() { LOG_BUTTON("", "Safe Mode Button", this); } + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h new file mode 100644 index 0000000000..63e0d1755e --- /dev/null +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ota/ota_component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace safe_mode { + +class SafeModeButton : public button::Button, public Component { + public: + void dump_config() override; + void set_ota(ota::OTAComponent *ota); + + protected: + ota::OTAComponent *ota_; + void press_action() override; +}; + +} // namespace safe_mode +} // namespace esphome diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp index 2348c88938..9c35d306ad 100644 --- a/esphome/components/sdm_meter/sdm_meter.cpp +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -57,15 +57,19 @@ void SDMMeter::on_modbus_data(const std::vector &data) { phase.phase_angle_sensor_->publish_state(phase_angle); } + float total_power = sdm_meter_get_float(SDM_TOTAL_SYSTEM_POWER * 2); float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2); float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2); float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2); float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2); float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2); - ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh", frequency, - import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy); + ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh, T.P=%.3f W", + frequency, import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy, + total_power); + if (this->total_power_sensor_ != nullptr) + this->total_power_sensor_->publish_state(total_power); if (this->frequency_sensor_ != nullptr) this->frequency_sensor_->publish_state(frequency); if (this->import_active_energy_sensor_ != nullptr) @@ -95,6 +99,7 @@ void SDMMeter::dump_config() { LOG_SENSOR(" ", "Power Factor", phase.power_factor_sensor_); LOG_SENSOR(" ", "Phase Angle", phase.phase_angle_sensor_); } + LOG_SENSOR(" ", "Total Power", this->total_power_sensor_); LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h index 07ebe65bb7..66f0fb8c5e 100644 --- a/esphome/components/sdm_meter/sdm_meter.h +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -37,6 +37,7 @@ class SDMMeter : public PollingComponent, public modbus::ModbusDevice { this->phases_[phase].setup = true; this->phases_[phase].phase_angle_sensor_ = phase_angle_sensor; } + void set_total_power_sensor(sensor::Sensor *total_power_sensor) { this->total_power_sensor_ = total_power_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } void set_import_active_energy_sensor(sensor::Sensor *import_active_energy_sensor) { this->import_active_energy_sensor_ = import_active_energy_sensor; @@ -69,6 +70,7 @@ class SDMMeter : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *phase_angle_sensor_{nullptr}; } phases_[3]; sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *total_power_sensor_{nullptr}; sensor::Sensor *import_active_energy_sensor_{nullptr}; sensor::Sensor *export_active_energy_sensor_{nullptr}; sensor::Sensor *import_reactive_energy_sensor_{nullptr}; diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 87c99c9152..4f439ac506 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_CURRENT, CONF_EXPORT_ACTIVE_ENERGY, CONF_EXPORT_REACTIVE_ENERGY, + CONF_TOTAL_POWER, CONF_FREQUENCY, CONF_ID, CONF_IMPORT_ACTIVE_ENERGY, @@ -98,6 +99,12 @@ CONFIG_SCHEMA = ( accuracy_decimals=3, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=2, @@ -132,6 +139,10 @@ async def to_code(config): await cg.register_component(var, config) await modbus.register_modbus_device(var, config) + if CONF_TOTAL_POWER in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_POWER]) + cg.add(var.set_total_power_sensor(sens)) + if CONF_FREQUENCY in config: sens = await sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_frequency_sensor(sens)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index d9d226aab6..14a15da2f1 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, + CONF_QUANTILE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_STATE_CLASS, @@ -151,6 +152,7 @@ SensorPublishAction = sensor_ns.class_("SensorPublishAction", automation.Action) # Filters Filter = sensor_ns.class_("Filter") +QuantileFilter = sensor_ns.class_("QuantileFilter", Filter) MedianFilter = sensor_ns.class_("MedianFilter", Filter) MinFilter = sensor_ns.class_("MinFilter", Filter) MaxFilter = sensor_ns.class_("MaxFilter", Filter) @@ -285,6 +287,30 @@ async def filter_out_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) +QUANTILE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float, + } + ), + validate_send_first_at, +) + + +@FILTER_REGISTRY.register("quantile", QuantileFilter, QUANTILE_SCHEMA) +async def quantile_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_WINDOW_SIZE], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + config[CONF_QUANTILE], + ) + + MEDIAN_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 321e3a4a4f..7a8a557273 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,7 +1,8 @@ #include "filter.h" -#include "sensor.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "sensor.h" +#include namespace esphome { namespace sensor { @@ -66,6 +67,41 @@ optional MedianFilter::new_value(float value) { return {}; } +// QuantileFilter +QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile) + : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {} +void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; } +optional QuantileFilter::new_value(float value) { + if (!std::isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float result = 0.0f; + if (!this->queue_.empty()) { + std::deque quantile_queue = this->queue_; + sort(quantile_queue.begin(), quantile_queue.end()); + + size_t queue_size = quantile_queue.size(); + size_t position = ceilf(queue_size * this->quantile_) - 1; + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position, queue_size); + result = quantile_queue[position]; + } + + ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING", this, result); + return result; + } + return {}; +} + // MinFilter MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at) : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index d595e419a6..0ed7ce4801 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -42,6 +42,37 @@ class Filter { Sensor *parent_{nullptr}; }; +/** Simple quantile filter. + * + * Takes the quantile of the last values and pushes it out every . + */ +class QuantileFilter : public Filter { + public: + /** Construct a QuantileFilter. + * + * @param window_size The number of values that should be used in quantile calculation. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + * @param quantile float 0..1 to pick the requested quantile. Defaults to 0.9. + */ + explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + void set_quantile(float quantile); + + protected: + std::deque queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; + float quantile_; +}; + /** Simple median filter. * * Takes the median of the last values and pushes it out every . diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 793ae170c3..73730f6482 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -24,7 +24,10 @@ Sensor::Sensor() : Sensor("") {} std::string Sensor::get_unit_of_measurement() { if (this->unit_of_measurement_.has_value()) return *this->unit_of_measurement_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->unit_of_measurement(); +#pragma GCC diagnostic pop } void Sensor::set_unit_of_measurement(const std::string &unit_of_measurement) { this->unit_of_measurement_ = unit_of_measurement; @@ -34,7 +37,10 @@ std::string Sensor::unit_of_measurement() { return ""; } int8_t Sensor::get_accuracy_decimals() { if (this->accuracy_decimals_.has_value()) return *this->accuracy_decimals_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->accuracy_decimals(); +#pragma GCC diagnostic pop } void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } int8_t Sensor::accuracy_decimals() { return 0; } @@ -42,7 +48,10 @@ int8_t Sensor::accuracy_decimals() { return 0; } std::string Sensor::get_device_class() { if (this->device_class_.has_value()) return *this->device_class_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->device_class(); +#pragma GCC diagnostic pop } void Sensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } std::string Sensor::device_class() { return ""; } @@ -51,7 +60,10 @@ void Sensor::set_state_class(StateClass state_class) { this->state_class_ = stat StateClass Sensor::get_state_class() { if (this->state_class_.has_value()) return *this->state_class_; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return this->state_class(); +#pragma GCC diagnostic pop } StateClass Sensor::state_class() { return StateClass::STATE_CLASS_NONE; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 6cab46f7f9..794aecca95 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -151,15 +151,19 @@ class Sensor : public EntityBase { protected: /// Override this to set the default unit of measurement. + ESPDEPRECATED("unit_of_measurement() is deprecated, set property during config validation instead.", "2022.01") virtual std::string unit_of_measurement(); // NOLINT /// Override this to set the default accuracy in decimals. + ESPDEPRECATED("accuracy_decimals() is deprecated, set property during config validation instead.", "2022.01") virtual int8_t accuracy_decimals(); // NOLINT /// Override this to set the default device class. + ESPDEPRECATED("device_class() is deprecated, set property during config validation instead.", "2022.01") virtual std::string device_class(); // NOLINT /// Override this to set the default state class. + ESPDEPRECATED("state_class() is deprecated, set property during config validation instead.", "2022.01") virtual StateClass state_class(); // NOLINT uint32_t hash_base() override; diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 9561efcde2..da6659c90f 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -211,7 +211,7 @@ uint16_t SGP40Component::measure_raw_() { ESP_LOGD(TAG, "write error"); return UINT16_MAX; } - delay(250); // NOLINT + delay(30); uint16_t raw_data[1]; if (!this->read_data_(raw_data, 1)) { diff --git a/esphome/components/shutdown/__init__.py b/esphome/components/shutdown/__init__.py index f70ffa9520..480a6f3e31 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@esphome/core"] +CODEOWNERS = ["@esphome/core", "@jsuanet"] diff --git a/esphome/components/shutdown/button/__init__.py b/esphome/components/shutdown/button/__init__.py new file mode 100644 index 0000000000..51cd6d6da2 --- /dev/null +++ b/esphome/components/shutdown/button/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_ID, + ENTITY_CATEGORY_CONFIG, + ICON_POWER, +) + +shutdown_ns = cg.esphome_ns.namespace("shutdown") +ShutdownButton = shutdown_ns.class_("ShutdownButton", button.Button, cg.Component) + +CONFIG_SCHEMA = ( + button.button_schema(entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER) + .extend({cv.GenerateID(): cv.declare_id(ShutdownButton)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) diff --git a/esphome/components/shutdown/button/shutdown_button.cpp b/esphome/components/shutdown/button/shutdown_button.cpp new file mode 100644 index 0000000000..be88a10d49 --- /dev/null +++ b/esphome/components/shutdown/button/shutdown_button.cpp @@ -0,0 +1,33 @@ +#include "shutdown_button.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef USE_ESP32 +#include +#endif +#ifdef USE_ESP8266 +#include +#endif + +namespace esphome { +namespace shutdown { + +static const char *const TAG = "shutdown.button"; + +void ShutdownButton::dump_config() { LOG_BUTTON("", "Shutdown Button", this); } +void ShutdownButton::press_action() { + ESP_LOGI(TAG, "Shutting down..."); + // Let MQTT settle a bit + delay(100); // NOLINT + App.run_safe_shutdown_hooks(); +#ifdef USE_ESP8266 + ESP.deepSleep(0); // NOLINT(readability-static-accessed-through-instance) +#endif +#ifdef USE_ESP32 + esp_deep_sleep_start(); +#endif +} + +} // namespace shutdown +} // namespace esphome diff --git a/esphome/components/shutdown/button/shutdown_button.h b/esphome/components/shutdown/button/shutdown_button.h new file mode 100644 index 0000000000..d0094c899d --- /dev/null +++ b/esphome/components/shutdown/button/shutdown_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace shutdown { + +class ShutdownButton : public button::Button, public Component { + public: + void dump_config() override; + + protected: + void press_action() override; +}; + +} // namespace shutdown +} // namespace esphome diff --git a/esphome/components/shutdown/switch.py b/esphome/components/shutdown/switch/__init__.py similarity index 100% rename from esphome/components/shutdown/switch.py rename to esphome/components/shutdown/switch/__init__.py diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/switch/shutdown_switch.cpp similarity index 100% rename from esphome/components/shutdown/shutdown_switch.cpp rename to esphome/components/shutdown/switch/shutdown_switch.cpp diff --git a/esphome/components/shutdown/shutdown_switch.h b/esphome/components/shutdown/switch/shutdown_switch.h similarity index 100% rename from esphome/components/shutdown/shutdown_switch.h rename to esphome/components/shutdown/switch/shutdown_switch.h diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 0887b8640f..4143627084 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All( .extend(uart.UART_DEVICE_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "sim800l", baud_rate=9600, require_tx=True, require_rx=True + "sim800l", require_tx=True, require_rx=True ) diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py index 4f44582eba..0ce1c9f9e2 100644 --- a/esphome/components/slow_pwm/output.py +++ b/esphome/components/slow_pwm/output.py @@ -2,15 +2,37 @@ from esphome import pins, core from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_PIN, CONF_PERIOD +from esphome import automation +from esphome.const import ( + CONF_ID, + CONF_PIN, + CONF_PERIOD, + CONF_TURN_ON_ACTION, + CONF_TURN_OFF_ACTION, +) slow_pwm_ns = cg.esphome_ns.namespace("slow_pwm") SlowPWMOutput = slow_pwm_ns.class_("SlowPWMOutput", output.FloatOutput, cg.Component) +CONF_STATE_CHANGE_ACTION = "state_change_action" + CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(SlowPWMOutput), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, + cv.Inclusive( + CONF_TURN_ON_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Inclusive( + CONF_TURN_OFF_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation( + single=True + ), cv.Required(CONF_PERIOD): cv.All( cv.positive_time_period_milliseconds, cv.Range(min=core.TimePeriod(milliseconds=100)), @@ -23,7 +45,21 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) + if CONF_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + if CONF_STATE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_state_change_trigger(), + [(bool, "state")], + config[CONF_STATE_CHANGE_ACTION], + ) + if CONF_TURN_ON_ACTION in config: + await automation.build_automation( + var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] + ) + await automation.build_automation( + var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] + ) - pin = await cg.gpio_pin_expression(config[CONF_PIN]) - cg.add(var.set_pin(pin)) cg.add(var.set_period(config[CONF_PERIOD])) diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 9b2589e735..573adbe3dc 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -7,34 +7,59 @@ namespace slow_pwm { static const char *const TAG = "output.slow_pwm"; void SlowPWMOutput::setup() { - this->pin_->setup(); + if (this->pin_) + this->pin_->setup(); this->turn_off(); } +/// turn on/off the configured output +void SlowPWMOutput::set_output_state_(bool new_state) { + if (this->pin_) { + this->pin_->digital_write(new_state); + } + if (new_state != current_state_) { + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(new_state); + } + if (new_state) { + if (this->turn_on_trigger_) + this->turn_on_trigger_->trigger(); + } else { + if (this->turn_off_trigger_) + this->turn_off_trigger_->trigger(); + } + current_state_ = new_state; + } +} + void SlowPWMOutput::loop() { uint32_t now = millis(); float scaled_state = this->state_ * this->period_; - if (now - this->period_start_time_ > this->period_) { + if (now - this->period_start_time_ >= this->period_) { ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state); this->period_start_time_ += this->period_; } if (scaled_state > now - this->period_start_time_) { - this->pin_->digital_write(true); + this->set_output_state_(true); } else { - this->pin_->digital_write(false); + this->set_output_state_(false); } } void SlowPWMOutput::dump_config() { ESP_LOGCONFIG(TAG, "Slow PWM Output:"); LOG_PIN(" Pin: ", this->pin_); + if (this->state_change_trigger_) + ESP_LOGCONFIG(TAG, " State change automation configured"); + if (this->turn_on_trigger_) + ESP_LOGCONFIG(TAG, " Turn on automation configured"); + if (this->turn_off_trigger_) + ESP_LOGCONFIG(TAG, " Turn off automation configured"); ESP_LOGCONFIG(TAG, " Period: %d ms", this->period_); LOG_FLOAT_OUTPUT(this); } -void SlowPWMOutput::write_state(float state) { this->state_ = state; } - } // namespace slow_pwm } // namespace esphome diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index f0524f36d8..d5c5883f25 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -1,5 +1,5 @@ #pragma once - +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" @@ -11,19 +11,42 @@ class SlowPWMOutput : public output::FloatOutput, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; }; void set_period(unsigned int period) { period_ = period; }; - /// Initialize pin void setup() override; void dump_config() override; /// HARDWARE setup_priority float get_setup_priority() const override { return setup_priority::HARDWARE; } - protected: - void write_state(float state) override; - void loop() override; + Trigger<> *get_turn_on_trigger() { + // Lazy create + if (!this->turn_on_trigger_) + this->turn_on_trigger_ = make_unique>(); + return this->turn_on_trigger_.get(); + } + Trigger<> *get_turn_off_trigger() { + if (!this->turn_off_trigger_) + this->turn_off_trigger_ = make_unique>(); + return this->turn_off_trigger_.get(); + } - GPIOPin *pin_; + Trigger *get_state_change_trigger() { + if (!this->state_change_trigger_) + this->state_change_trigger_ = make_unique>(); + return this->state_change_trigger_.get(); + } + + protected: + void loop() override; + void write_state(float state) override { state_ = state; } + /// turn on/off the configured output + void set_output_state_(bool state); + + GPIOPin *pin_{nullptr}; + std::unique_ptr> turn_on_trigger_{nullptr}; + std::unique_ptr> turn_off_trigger_{nullptr}; + std::unique_ptr> state_change_trigger_{nullptr}; float state_{0}; + bool current_state_{false}; unsigned int period_start_time_{0}; unsigned int period_{5000}; }; diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index e4f62e5ff9..f2e4ef5811 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -31,6 +31,7 @@ MODELS = { "SH1106_128X64": SSD1306Model.SH1106_MODEL_128_64, "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, + "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } @@ -61,8 +62,8 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, cv.Optional(CONF_FLIP_X, default=True): cv.boolean, cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, - cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=15), - cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=15), + cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 4b9feb10ce..5ff220fce9 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -96,6 +96,7 @@ void SSD1306::setup() { case SSD1306_MODEL_64_48: case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: + case SH1107_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: this->command(0x12); @@ -111,7 +112,14 @@ void SSD1306::setup() { // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); - this->command(0x00); + switch (this->model_) { + case SH1107_MODEL_128_64: + this->command(0x35); + break; + default: + this->command(0x00); + break; + } // Display output follow RAM (0xA4) this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); @@ -198,6 +206,8 @@ void SSD1306::turn_off() { } int SSD1306::get_height_internal() { switch (this->model_) { + case SH1107_MODEL_128_64: + return 128; case SSD1306_MODEL_128_32: case SSD1306_MODEL_64_32: case SH1106_MODEL_128_32: @@ -232,6 +242,7 @@ int SSD1306::get_width_internal() { case SSD1306_MODEL_64_48: case SSD1306_MODEL_64_32: case SH1106_MODEL_64_48: + case SH1107_MODEL_128_64: return 64; default: return 0; @@ -289,6 +300,8 @@ const char *SSD1306::model_str_() { return "SH1106 96x16"; case SH1106_MODEL_64_48: return "SH1106 64x48"; + case SH1107_MODEL_128_64: + return "SH1107 128x64"; case SSD1305_MODEL_128_32: return "SSD1305 128x32"; case SSD1305_MODEL_128_64: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index c77b1985e4..5ab68143c7 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -17,6 +17,7 @@ enum SSD1306Model { SH1106_MODEL_128_64, SH1106_MODEL_96_16, SH1106_MODEL_64_48, + SH1107_MODEL_128_64, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, }; diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index c5178986f3..a0c2d80d16 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -265,8 +265,8 @@ void ST7735::setup() { height_ == 0 ? height_ = ST7735_TFTHEIGHT_160 : height_; width_ == 0 ? width_ = ST7735_TFTWIDTH_80 : width_; display_init_(RCMD2GREEN160X80); - colstart_ = 24; - rowstart_ = 0; // For default rotation 0 + colstart_ == 0 ? colstart_ = 24 : colstart_; + rowstart_ == 0 ? rowstart_ = 0 : rowstart_; } else { // colstart, rowstart left at default '0' values display_init_(RCMD2RED); diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index d0258d922c..5f32e7ff23 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -14,7 +14,7 @@ using st7920_writer_t = std::function; class ST7920 : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevice { + spi::DATA_RATE_200KHZ> { public: void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; } void set_height(uint16_t height) { this->height_ = height; } diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index e4d20719e1..b9b99b4147 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -34,7 +34,7 @@ void Switch::publish_state(bool state) { this->state = state != this->inverted_; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(state)); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); } bool Switch::assumed_state() { return false; } diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index 7999029f5a..609c62fd10 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -8,6 +8,8 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_INTENSITY, + CONF_INVERTED, + CONF_LENGTH, ) CODEOWNERS = ["@glmnet"] @@ -22,6 +24,8 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_INTENSITY, default=7): cv.All( cv.uint8_t, cv.Range(min=0, max=7) ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + cv.Optional(CONF_LENGTH, default=6): cv.All(cv.uint8_t, cv.Range(min=1, max=6)), cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DIO_PIN): pins.gpio_output_pin_schema, } @@ -39,6 +43,8 @@ async def to_code(config): cg.add(var.set_dio_pin(dio)) cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_length(config[CONF_LENGTH])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 488f3b6727..a21d2d438d 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -130,7 +130,9 @@ void TM1637Display::setup() { } void TM1637Display::dump_config() { ESP_LOGCONFIG(TAG, "TM1637:"); - ESP_LOGCONFIG(TAG, " INTENSITY: %d", this->intensity_); + ESP_LOGCONFIG(TAG, " Intensity: %d", this->intensity_); + ESP_LOGCONFIG(TAG, " Inverted: %d", this->inverted_); + ESP_LOGCONFIG(TAG, " Length: %d", this->length_); LOG_PIN(" CLK Pin: ", this->clk_pin_); LOG_PIN(" DIO Pin: ", this->dio_pin_); LOG_UPDATE_INTERVAL(this); @@ -173,8 +175,14 @@ void TM1637Display::display() { this->send_byte_(TM1637_I2C_COMM2); // Write the data bytes - for (auto b : this->buffer_) { - this->send_byte_(b); + if (this->inverted_) { + for (int8_t i = this->length_ - 1; i >= 0; i--) { + this->send_byte_(this->buffer_[i]); + } + } else { + for (auto b : this->buffer_) { + this->send_byte_(b); + } } this->stop_(); @@ -241,14 +249,27 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { } // Remap segments, for compatibility with MAX7219 segment definition which is // XABCDEFG, but TM1637 is // XGFEDCBA - data = ((data & 0x80) ? 0x80 : 0) | // no move X - ((data & 0x40) ? 0x1 : 0) | // A - ((data & 0x20) ? 0x2 : 0) | // B - ((data & 0x10) ? 0x4 : 0) | // C - ((data & 0x8) ? 0x8 : 0) | // D - ((data & 0x4) ? 0x10 : 0) | // E - ((data & 0x2) ? 0x20 : 0) | // F - ((data & 0x1) ? 0x40 : 0); // G + if (this->inverted_) { + // XABCDEFG > XGCBAFED + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x8 : 0) | // A + ((data & 0x20) ? 0x10 : 0) | // B + ((data & 0x10) ? 0x20 : 0) | // C + ((data & 0x8) ? 0x1 : 0) | // D + ((data & 0x4) ? 0x2 : 0) | // E + ((data & 0x2) ? 0x4 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + } else { + // XABCDEFG > XGFEDCBA + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x1 : 0) | // A + ((data & 0x20) ? 0x2 : 0) | // B + ((data & 0x10) ? 0x4 : 0) | // C + ((data & 0x8) ? 0x8 : 0) | // D + ((data & 0x4) ? 0x10 : 0) | // E + ((data & 0x2) ? 0x20 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + } if (*str == '.') { if (pos != start_pos) pos--; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 63b30ac13e..9b2f014ff9 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -41,6 +41,8 @@ class TM1637Display : public PollingComponent { uint8_t print(const char *str); void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_length(uint8_t length) { this->length_ = length; } void display(); @@ -62,6 +64,8 @@ class TM1637Display : public PollingComponent { GPIOPin *dio_pin_; GPIOPin *clk_pin_; uint8_t intensity_; + uint8_t length_; + bool inverted_; optional writer_{}; uint8_t buffer_[6] = {0}; }; diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 0c20ccd27c..1af8db8332 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -9,6 +9,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, CONF_METHOD, STATE_CLASS_TOTAL_INCREASING, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, ) from esphome.core.entity_helpers import inherit_property_from @@ -27,6 +29,15 @@ TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component ) + +def inherit_unit_of_measurement(uom, config): + return uom + "h" + + +def inherit_accuracy_decimals(decimals, config): + return decimals + 2 + + CONFIG_SCHEMA = ( sensor.sensor_schema( device_class=DEVICE_CLASS_ENERGY, @@ -54,11 +65,19 @@ FINAL_VALIDATE_SCHEMA = cv.All( { cv.Required(CONF_ID): cv.use_id(TotalDailyEnergy), cv.Optional(CONF_ICON): cv.icon, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): sensor.validate_unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS): sensor.validate_accuracy_decimals, cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), }, extra=cv.ALLOW_EXTRA, ), inherit_property_from(CONF_ICON, CONF_POWER_ID), + inherit_property_from( + CONF_UNIT_OF_MEASUREMENT, CONF_POWER_ID, transform=inherit_unit_of_measurement + ), + inherit_property_from( + CONF_ACCURACY_DECIMALS, CONF_POWER_ID, transform=inherit_accuracy_decimals + ), ) diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 498f65891e..a35edfd11b 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -25,8 +25,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } - std::string unit_of_measurement() override { return this->parent_->get_unit_of_measurement() + "h"; } - int8_t accuracy_decimals() override { return this->parent_->get_accuracy_decimals() + 2; } void loop() override; void publish_state_and_save(float state); diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.cpp b/esphome/components/ttp229_lsf/ttp229_lsf.cpp index 21c7b02740..773d51b76e 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.cpp +++ b/esphome/components/ttp229_lsf/ttp229_lsf.cpp @@ -35,7 +35,7 @@ void TTP229LSFComponent::loop() { } touched = i2c::i2ctohs(touched); this->status_clear_warning(); - touched = reverse_bits_16(touched); + touched = reverse_bits(touched); for (auto *channel : this->channels_) { channel->process(touched); } diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 133ee1e557..ecd3802839 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -37,9 +37,9 @@ void TuyaLight::setup() { } if (rgb_id_.has_value()) { this->parent_->register_listener(*this->rgb_id_, [this](const TuyaDatapoint &datapoint) { - auto red = parse_hex(datapoint.value_string, 0, 2); - auto green = parse_hex(datapoint.value_string, 2, 2); - auto blue = parse_hex(datapoint.value_string, 4, 2); + auto red = parse_hex(datapoint.value_string.substr(0, 2)); + auto green = parse_hex(datapoint.value_string.substr(2, 2)); + auto blue = parse_hex(datapoint.value_string.substr(4, 2)); if (red.has_value() && green.has_value() && blue.has_value()) { auto call = this->state_->make_call(); call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); @@ -48,9 +48,9 @@ void TuyaLight::setup() { }); } else if (hsv_id_.has_value()) { this->parent_->register_listener(*this->hsv_id_, [this](const TuyaDatapoint &datapoint) { - auto hue = parse_hex(datapoint.value_string, 0, 4); - auto saturation = parse_hex(datapoint.value_string, 4, 4); - auto value = parse_hex(datapoint.value_string, 8, 4); + auto hue = parse_hex(datapoint.value_string.substr(0, 4)); + auto saturation = parse_hex(datapoint.value_string.substr(4, 4)); + auto value = parse_hex(datapoint.value_string.substr(8, 4)); if (hue.has_value() && saturation.has_value() && value.has_value()) { float red, green, blue; hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py new file mode 100644 index 0000000000..12c0c0f6e5 --- /dev/null +++ b/esphome/components/tuya/number/__init__.py @@ -0,0 +1,54 @@ +from esphome.components import number +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_ID, + CONF_NUMBER_DATAPOINT, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_STEP, +) +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@frankiboy1"] + +TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component) + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaNumber), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_NUMBER_DATAPOINT): cv.uint8_t, + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.positive_float, + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await number.register_number( + var, + config, + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], + ) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp new file mode 100644 index 0000000000..5c7cafbf7a --- /dev/null +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -0,0 +1,38 @@ +#include "esphome/core/log.h" +#include "tuya_number.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.number"; + +void TuyaNumber::setup() { + this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { + if (datapoint.type == TuyaDatapointType::INTEGER) { + ESP_LOGV(TAG, "MCU reported number %u is: %d", datapoint.id, datapoint.value_int); + this->publish_state(datapoint.value_int); + } else if (datapoint.type == TuyaDatapointType::ENUM) { + ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); + this->publish_state(datapoint.value_enum); + } + this->type_ = datapoint.type; + }); +} + +void TuyaNumber::control(float value) { + ESP_LOGV(TAG, "Setting number %u: %f", this->number_id_, value); + if (this->type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(this->number_id_, value); + } else if (this->type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(this->number_id_, value); + } + this->publish_state(value); +} + +void TuyaNumber::dump_config() { + LOG_NUMBER("", "Tuya Number", this); + ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h new file mode 100644 index 0000000000..7cca9fc646 --- /dev/null +++ b/esphome/components/tuya/number/tuya_number.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace tuya { + +class TuyaNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + void control(float value) override; + + Tuya *parent_; + uint8_t number_id_{0}; + TuyaDatapointType type_{}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index e939225453..0b51ba90c4 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -14,7 +14,7 @@ void TuyaTextSensor::setup() { this->publish_state(datapoint.value_string); break; case TuyaDatapointType::RAW: { - std::string data = hexencode(datapoint.value_raw); + std::string data = format_hex_pretty(datapoint.value_raw); ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, data.c_str()); this->publish_state(data); break; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 404a70a80e..7ff8c66c44 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -34,7 +34,7 @@ void Tuya::dump_config() { } for (auto &info : this->datapoints_) { if (info.type == TuyaDatapointType::RAW) - ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, hexencode(info.value_raw).c_str()); + ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, format_hex_pretty(info.value_raw).c_str()); else if (info.type == TuyaDatapointType::BOOLEAN) ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool)); else if (info.type == TuyaDatapointType::INTEGER) @@ -104,7 +104,7 @@ bool Tuya::validate_message_() { // valid message const uint8_t *message_data = data + 6; ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, - hexencode(message_data, length).c_str(), static_cast(this->init_state_)); + format_hex_pretty(message_data, length).c_str(), static_cast(this->init_state_)); this->handle_command_(command, version, message_data, length); // return false to reset rx buffer @@ -253,7 +253,7 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { switch (datapoint.type) { case TuyaDatapointType::RAW: datapoint.value_raw = std::vector(data, data + data_len); - ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, hexencode(datapoint.value_raw).c_str()); + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, format_hex_pretty(datapoint.value_raw).c_str()); break; case TuyaDatapointType::BOOLEAN: if (data_len != 1) { @@ -348,7 +348,7 @@ void Tuya::send_raw_command_(TuyaCommand command) { } ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), - version, hexencode(command.payload).c_str(), static_cast(this->init_state_)); + version, format_hex_pretty(command.payload).c_str(), static_cast(this->init_state_)); this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo}); if (!command.payload.empty()) @@ -526,7 +526,7 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType } void Tuya::set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced) { - ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, hexencode(value).c_str()); + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty(value).c_str()); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 64f5597a65..1d1644dc25 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import core, pins from esphome.components import display, spi from esphome.const import ( CONF_BUSY_PIN, @@ -10,6 +10,7 @@ from esphome.const import ( CONF_LAMBDA, CONF_MODEL, CONF_PAGES, + CONF_RESET_DURATION, CONF_RESET_PIN, ) @@ -95,6 +96,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, + cv.Optional(CONF_RESET_DURATION): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), } ) .extend(cv.polling_component_schema("1s")) @@ -135,3 +140,5 @@ async def to_code(config): cg.add(var.set_busy_pin(reset)) if CONF_FULL_UPDATE_EVERY in config: cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + if CONF_RESET_DURATION in config: + cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index a1e2f6037a..4de2ac7d97 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -16,6 +16,7 @@ class WaveshareEPaper : public PollingComponent, float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } + void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } void command(uint8_t value); void data(uint8_t value); @@ -45,13 +46,14 @@ class WaveshareEPaper : public PollingComponent, void reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(200); // NOLINT + delay(reset_duration_); // NOLINT this->reset_pin_->digital_write(true); delay(200); // NOLINT } } uint32_t get_buffer_length_(); + uint32_t reset_duration_{200}; void start_command_(); void end_command_(); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1e7696edfb..4cc77da256 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -316,7 +316,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::sensor_json(sensor::Sensor *obj, float value) { - return json::build_json([obj, value](JsonObject &root) { + return json::build_json([obj, value](JsonObject root) { root["id"] = "sensor-" + obj->get_object_id(); std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); if (!obj->get_unit_of_measurement().empty()) @@ -342,7 +342,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const request->send(404); } std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) { - return json::build_json([obj, value](JsonObject &root) { + return json::build_json([obj, value](JsonObject root) { root["id"] = "text_sensor-" + obj->get_object_id(); root["state"] = value; root["value"] = value; @@ -355,7 +355,7 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { this->events_.send(this->switch_json(obj, state).c_str(), "state"); } std::string WebServer::switch_json(switch_::Switch *obj, bool value) { - return json::build_json([obj, value](JsonObject &root) { + return json::build_json([obj, value](JsonObject root) { root["id"] = "switch-" + obj->get_object_id(); root["state"] = value ? "ON" : "OFF"; root["value"] = value; @@ -410,7 +410,7 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool s this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state"); } std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) { - return json::build_json([obj, value](JsonObject &root) { + return json::build_json([obj, value](JsonObject root) { root["id"] = "binary_sensor-" + obj->get_object_id(); root["state"] = value ? "ON" : "OFF"; root["value"] = value; @@ -431,7 +431,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con #ifdef USE_FAN void WebServer::on_fan_update(fan::FanState *obj) { this->events_.send(this->fan_json(obj).c_str(), "state"); } std::string WebServer::fan_json(fan::FanState *obj) { - return json::build_json([obj](JsonObject &root) { + return json::build_json([obj](JsonObject root) { root["id"] = "fan-" + obj->get_object_id(); root["state"] = obj->state ? "ON" : "OFF"; root["value"] = obj->state; @@ -580,7 +580,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::light_json(light::LightState *obj) { - return json::build_json([obj](JsonObject &root) { + return json::build_json([obj](JsonObject root) { root["id"] = "light-" + obj->get_object_id(); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; light::LightJSONSchema::dump_json(*obj, root); @@ -632,7 +632,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::cover_json(cover::Cover *obj) { - return json::build_json([obj](JsonObject &root) { + return json::build_json([obj](JsonObject root) { root["id"] = "cover-" + obj->get_object_id(); root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN"; root["value"] = obj->position; @@ -659,7 +659,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::number_json(number::Number *obj, float value) { - return json::build_json([obj, value](JsonObject &root) { + return json::build_json([obj, value](JsonObject root) { root["id"] = "number-" + obj->get_object_id(); char buffer[64]; snprintf(buffer, sizeof(buffer), "%f", value); @@ -703,7 +703,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::select_json(select::Select *obj, const std::string &value) { - return json::build_json([obj, value](JsonObject &root) { + return json::build_json([obj, value](JsonObject root) { root["id"] = "select-" + obj->get_object_id(); root["state"] = value; root["value"] = value; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 7588198c70..583b68a77b 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -217,7 +217,7 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) { ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); - ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); return false; } @@ -274,12 +274,12 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c memcpy(mac_address + 4, mac_reverse + 1, 1); memcpy(mac_address + 5, mac_reverse, 1); ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); - ESP_LOGVV(TAG, " MAC address : %s", hexencode(mac_address, 6).c_str()); - ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); - ESP_LOGVV(TAG, " Key : %s", hexencode(vector.key, vector.keysize).c_str()); - ESP_LOGVV(TAG, " Iv : %s", hexencode(vector.iv, vector.ivsize).c_str()); - ESP_LOGVV(TAG, " Cipher : %s", hexencode(vector.ciphertext, vector.datasize).c_str()); - ESP_LOGVV(TAG, " Tag : %s", hexencode(vector.tag, vector.tagsize).c_str()); + ESP_LOGVV(TAG, " MAC address : %s", format_hex_pretty(mac_address, 6).c_str()); + ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); + ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); + ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); + ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str()); + ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str()); mbedtls_ccm_free(&ctx); return false; } @@ -295,7 +295,7 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c raw[0] &= ~0x08; ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); - ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", hexencode(raw.data() + cipher_pos, vector.datasize).c_str(), + ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(), static_cast(raw[4])); mbedtls_ccm_free(&ctx); diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp index 97bbd6e6d6..baf9cb8075 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_cgd1"; void XiaomiCGD1::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi CGD1"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp index a97ca93206..c74794f4f4 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_cgdk2"; void XiaomiCGDK2::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi CGDK2"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index e1f83e4ddd..c20c7578d0 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_cgg1"; void XiaomiCGG1::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi CGG1"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp index 547cc7c114..d0319c9474 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_lywsd03mmc"; void XiaomiLYWSD03MMC::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp index 0cad5c67b2..9ec2b10e12 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "xiaomi_mhoc401"; void XiaomiMHOC401::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi MHOC401"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); diff --git a/esphome/const.py b/esphome/const.py index 9ed0975cd4..fdc880caf3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.3" +__version__ = "2022.1.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -402,6 +402,7 @@ CONF_NUM_CHIPS = "num_chips" CONF_NUM_LEDS = "num_leds" CONF_NUM_SCANS = "num_scans" CONF_NUMBER = "number" +CONF_NUMBER_DATAPOINT = "number_datapoint" CONF_OFF_MODE = "off_mode" CONF_OFFSET = "offset" CONF_ON = "on" @@ -518,6 +519,7 @@ CONF_PULLDOWN = "pulldown" CONF_PULLUP = "pullup" CONF_PULSE_LENGTH = "pulse_length" CONF_QOS = "qos" +CONF_QUANTILE = "quantile" CONF_RADON = "radon" CONF_RADON_LONG_TERM = "radon_long_term" CONF_RANDOM = "random" @@ -539,6 +541,7 @@ CONF_REFERENCE_TEMPERATURE = "reference_temperature" CONF_REFRESH = "refresh" CONF_REPEAT = "repeat" CONF_REPOSITORY = "repository" +CONF_RESET_DURATION = "reset_duration" CONF_RESET_PIN = "reset_pin" CONF_RESIZE = "resize" CONF_RESOLUTION = "resolution" @@ -596,6 +599,7 @@ CONF_SHOW_VALUES = "show_values" CONF_SHUNT_RESISTANCE = "shunt_resistance" CONF_SHUNT_VOLTAGE = "shunt_voltage" CONF_SHUTDOWN_MESSAGE = "shutdown_message" +CONF_SIGNAL_STRENGTH = "signal_strength" CONF_SINGLE_LIGHT_ID = "single_light_id" CONF_SIZE = "size" CONF_SLEEP_DURATION = "sleep_duration" @@ -681,6 +685,7 @@ CONF_TOLERANCE = "tolerance" CONF_TOPIC = "topic" CONF_TOPIC_PREFIX = "topic_prefix" CONF_TOTAL = "total" +CONF_TOTAL_POWER = "total_power" CONF_TRACES = "traces" CONF_TRANSITION_LENGTH = "transition_length" CONF_TRIGGER_ID = "trigger_id" @@ -831,6 +836,7 @@ UNIT_MINUTE = "min" UNIT_OHM = "Ω" UNIT_PARTS_PER_BILLION = "ppb" UNIT_PARTS_PER_MILLION = "ppm" +UNIT_PASCAL = "Pa" UNIT_PERCENT = "%" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" diff --git a/esphome/core/automation.h b/esphome/core/automation.h index e5460bef34..f43fb98f20 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -21,10 +21,10 @@ template class TemplatableValue { public: TemplatableValue() : type_(EMPTY) {} - template::value, int> = 0> + template::value, int> = 0> TemplatableValue(F value) : type_(VALUE), value_(value) {} - template::value, int> = 0> + template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA), f_(f) {} bool has_value() { return this->type_ != EMPTY; } diff --git a/esphome/core/datatypes.h b/esphome/core/datatypes.h new file mode 100644 index 0000000000..5356be6b52 --- /dev/null +++ b/esphome/core/datatypes.h @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include "esphome/core/helpers.h" + +namespace esphome { + +namespace internal { + +/// Wrapper class for memory using big endian data layout, transparently converting it to native order. +template class BigEndianLayout { + public: + constexpr14 operator T() { return convert_big_endian(val_); } + + private: + T val_; +} __attribute__((packed)); + +/// Wrapper class for memory using big endian data layout, transparently converting it to native order. +template class LittleEndianLayout { + public: + constexpr14 operator T() { return convert_little_endian(val_); } + + private: + T val_; +} __attribute__((packed)); + +} // namespace internal + +/// 24-bit unsigned integer type, transparently converting to 32-bit. +struct uint24_t { // NOLINT(readability-identifier-naming) + operator uint32_t() { return val; } + uint32_t val : 24; +} __attribute__((packed)); + +/// 24-bit signed integer type, transparently converting to 32-bit. +struct int24_t { // NOLINT(readability-identifier-naming) + operator int32_t() { return val; } + int32_t val : 24; +} __attribute__((packed)); + +// Integer types in big or little endian data layout. +using uint64_be_t = internal::BigEndianLayout; +using uint32_be_t = internal::BigEndianLayout; +using uint24_be_t = internal::BigEndianLayout; +using uint16_be_t = internal::BigEndianLayout; +using int64_be_t = internal::BigEndianLayout; +using int32_be_t = internal::BigEndianLayout; +using int24_be_t = internal::BigEndianLayout; +using int16_be_t = internal::BigEndianLayout; +using uint64_le_t = internal::LittleEndianLayout; +using uint32_le_t = internal::LittleEndianLayout; +using uint24_le_t = internal::LittleEndianLayout; +using uint16_le_t = internal::LittleEndianLayout; +using int64_le_t = internal::LittleEndianLayout; +using int32_le_t = internal::LittleEndianLayout; +using int24_le_t = internal::LittleEndianLayout; +using int16_le_t = internal::LittleEndianLayout; + +} // namespace esphome diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index b2dbe2116e..f921711ec2 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -3,29 +3,54 @@ import esphome.final_validate as fv from esphome.const import CONF_ID -def inherit_property_from(property_to_inherit, parent_id_property): +def inherit_property_from(property_to_inherit, parent_id_property, transform=None): """Validator that inherits a configuration property from another entity, for use with FINAL_VALIDATE_SCHEMA. - If a property is already set, it will not be inherited. - Keyword arguments: - property_to_inherit -- the name of the property to inherit, e.g. CONF_ICON - parent_id_property -- the name of the property that holds the ID of the parent, e.g. CONF_POWER_ID + property_to_inherit -- the name or path of the property to inherit, e.g. CONF_ICON or [CONF_SENSOR, 0, CONF_ICON] + (the parent must exist, otherwise nothing is done). + parent_id_property -- the name or path of the property that holds the ID of the parent, e.g. CONF_POWER_ID or + [CONF_SENSOR, 1, CONF_POWER_ID]. """ + def _walk_config(config, path): + walk = [path] if not isinstance(path, list) else path + for item_or_index in walk: + config = config[item_or_index] + return config + def inherit_property(config): - if property_to_inherit not in config: + # Split the property into its path and name + if not isinstance(property_to_inherit, list): + property_path, property = [], property_to_inherit + else: + property_path, property = property_to_inherit[:-1], property_to_inherit[-1] + + # Check if the property to inherit is accessible + try: + config_part = _walk_config(config, property_path) + except KeyError: + return config + + # Only inherit the property if it does not exist yet + if property not in config_part: fconf = fv.full_config.get() # Get config for the parent entity - path = fconf.get_path_for_id(config[parent_id_property])[:-1] - parent_config = fconf.get_config_for_path(path) + parent_id = _walk_config(config, parent_id_property) + parent_path = fconf.get_path_for_id(parent_id)[:-1] + parent_config = fconf.get_config_for_path(parent_path) # If parent sensor has the property set, inherit it - if property_to_inherit in parent_config: + if property in parent_config: path = fconf.get_path_for_id(config[CONF_ID])[:-1] - this_config = fconf.get_config_for_path(path) - this_config[property_to_inherit] = parent_config[property_to_inherit] + this_config = _walk_config( + fconf.get_config_for_path(path), property_path + ) + value = parent_config[property] + if transform: + value = transform(value, config) + this_config[property] = value return config diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6678eddbff..e15e3a8ea3 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include #include +#include #include #include @@ -63,45 +64,6 @@ void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } std::string generate_hostname(const std::string &base) { return base + std::string("-") + get_mac_address(); } -uint32_t random_uint32() { -#ifdef USE_ESP32 - return esp_random(); -#elif defined(USE_ESP8266) - return os_random(); -#endif -} - -double random_double() { return random_uint32() / double(UINT32_MAX); } - -float random_float() { return float(random_double()); } - -void fill_random(uint8_t *data, size_t len) { -#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) - esp_fill_random(data, len); -#elif defined(USE_ESP8266) - int err = os_get_random(data, len); - assert(err == 0); -#else -#error "No random source for this system config" -#endif -} - -static uint32_t fast_random_seed = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; } -uint32_t fast_random_32() { - fast_random_seed = (fast_random_seed * 2654435769ULL) + 40503ULL; - return fast_random_seed; -} -uint16_t fast_random_16() { - uint32_t rand32 = fast_random_32(); - return (rand32 & 0xFFFF) + (rand32 >> 16); -} -uint8_t fast_random_8() { - uint32_t rand32 = fast_random_32(); - return (rand32 & 0xFF) + ((rand32 >> 8) & 0xFF); -} - float gamma_correct(float value, float gamma) { if (value <= 0.0f) return 0.0f; @@ -129,18 +91,6 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } -std::string uint64_to_string(uint64_t num) { - char buffer[17]; - auto *address16 = reinterpret_cast(&num); - snprintf(buffer, sizeof(buffer), "%04X%04X%04X%04X", address16[3], address16[2], address16[1], address16[0]); - return std::string(buffer); -} -std::string uint32_to_string(uint32_t num) { - char buffer[9]; - auto *address16 = reinterpret_cast(&num); - snprintf(buffer, sizeof(buffer), "%04X%04X", address16[1], address16[0]); - return std::string(buffer); -} ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && strcasecmp(str, "on") == 0) @@ -187,95 +137,6 @@ void delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trig ; } -uint8_t reverse_bits_8(uint8_t x) { - x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); - x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); - x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4); - return x; -} - -uint16_t reverse_bits_16(uint16_t x) { - return uint16_t(reverse_bits_8(x & 0xFF) << 8) | uint16_t(reverse_bits_8(x >> 8)); -} -std::string to_string(const std::string &val) { return val; } -std::string to_string(int val) { - char buf[64]; - sprintf(buf, "%d", val); - return buf; -} -std::string to_string(long val) { // NOLINT - char buf[64]; - sprintf(buf, "%ld", val); - return buf; -} -std::string to_string(long long val) { // NOLINT - char buf[64]; - sprintf(buf, "%lld", val); - return buf; -} -std::string to_string(unsigned val) { // NOLINT - char buf[64]; - sprintf(buf, "%u", val); - return buf; -} -std::string to_string(unsigned long val) { // NOLINT - char buf[64]; - sprintf(buf, "%lu", val); - return buf; -} -std::string to_string(unsigned long long val) { // NOLINT - char buf[64]; - sprintf(buf, "%llu", val); - return buf; -} -std::string to_string(float val) { - char buf[64]; - sprintf(buf, "%f", val); - return buf; -} -std::string to_string(double val) { - char buf[64]; - sprintf(buf, "%f", val); - return buf; -} -std::string to_string(long double val) { - char buf[64]; - sprintf(buf, "%Lf", val); - return buf; -} - -optional parse_hex(const char chr) { - int out = chr; - if (out >= '0' && out <= '9') - return (out - '0'); - if (out >= 'A' && out <= 'F') - return (10 + (out - 'A')); - if (out >= 'a' && out <= 'f') - return (10 + (out - 'a')); - return {}; -} - -optional parse_hex(const std::string &str, size_t start, size_t length) { - if (str.length() < start) { - return {}; - } - size_t end = start + length; - if (str.length() < end) { - return {}; - } - int out = 0; - for (size_t i = start; i < end; i++) { - char chr = str[i]; - auto digit = parse_hex(chr); - if (!digit.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number, invalid character %c!", str.substr(start, length).c_str(), chr); - return {}; - } - out = (out << 4) | *digit; - } - return out; -} - uint32_t fnv1_hash(const std::string &str) { uint32_t hash = 2166136261UL; for (char c : str) { @@ -288,10 +149,6 @@ bool str_equals_case_insensitive(const std::string &a, const std::string &b) { return strcasecmp(a.c_str(), b.c_str()) == 0; } -template uint32_t reverse_bits(uint32_t x) { - return uint32_t(reverse_bits_16(x & 0xFFFF) << 16) | uint32_t(reverse_bits_16(x >> 16)); -} - static int high_freq_num_requests = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void HighFrequencyLoopRequester::start() { @@ -308,17 +165,6 @@ void HighFrequencyLoopRequester::stop() { } bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } -template T clamp(const T val, const T min, const T max) { - if (val < min) - return min; - if (val > max) - return max; - return val; -} -template uint8_t clamp(uint8_t, uint8_t, uint8_t); -template float clamp(float, float, float); -template int clamp(int, int, int); - float lerp(float completion, float start, float end) { return start + (end - start) * completion; } bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; } @@ -355,22 +201,6 @@ std::string str_sprintf(const char *fmt, ...) { return str; } -std::string hexencode(const uint8_t *data, uint32_t len) { - char buf[20]; - std::string res; - for (size_t i = 0; i < len; i++) { - if (i + 1 != len) { - sprintf(buf, "%02X.", data[i]); - } else { - sprintf(buf, "%02X ", data[i]); - } - res += buf; - } - sprintf(buf, "(%u)", len); - res += buf; - return res; -} - void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { float max_color_value = std::max(std::max(red, green), blue); float min_color_value = std::min(std::min(red, green), blue); @@ -445,6 +275,32 @@ IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } // --------------------------------------------------------------------------------------------------------------------- +// Mathematics + +uint32_t random_uint32() { +#ifdef USE_ESP32 + return esp_random(); +#elif defined(USE_ESP8266) + return os_random(); +#else +#error "No random source available for this configuration." +#endif +} +float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } +void random_bytes(uint8_t *data, size_t len) { +#ifdef USE_ESP32 + esp_fill_random(data, len); +#elif defined(USE_ESP8266) + if (os_get_random(data, len) != 0) { + ESP_LOGE(TAG, "Failed to generate random bytes!"); + } +#else +#error "No random source available for this configuration." +#endif +} + +// Strings + std::string str_truncate(const std::string &str, size_t length) { return str.length() > length ? str.substr(0, length) : str; } @@ -453,6 +309,16 @@ std::string str_until(const char *str, char ch) { return pos == nullptr ? std::string(str) : std::string(str, pos - str); } std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } +// wrapper around std::transform to run safely on functions from the ctype.h header +// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes +template std::string str_ctype_transform(const std::string &str) { + std::string result; + result.resize(str.length()); + std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); }); + return result; +} +std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } +std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } std::string str_snake_case(const std::string &str) { std::string result; result.resize(str.length()); @@ -468,4 +334,53 @@ std::string str_sanitize(const std::string &str) { return out; } +// Parsing & formatting + +size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { + uint8_t val; + size_t chars = std::min(length, 2 * count); + for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) { + if (*str >= '0' && *str <= '9') + val = *str - '0'; + else if (*str >= 'A' && *str <= 'F') + val = 10 + (*str - 'A'); + else if (*str >= 'a' && *str <= 'f') + val = 10 + (*str - 'a'); + else + return 0; + data[i >> 1] = !(i & 1) ? val << 4 : data[i >> 1] | val; + } + return chars; +} + +static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } +std::string format_hex(const uint8_t *data, size_t length) { + std::string ret; + ret.resize(length * 2); + for (size_t i = 0; i < length; i++) { + ret[2 * i] = format_hex_char((data[i] & 0xF0) >> 4); + ret[2 * i + 1] = format_hex_char(data[i] & 0x0F); + } + return ret; +} +std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } + +static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } +std::string format_hex_pretty(const uint8_t *data, size_t length) { + if (length == 0) + return ""; + std::string ret; + ret.resize(3 * length - 1); + for (size_t i = 0; i < length; i++) { + ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); + ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F); + if (i != length - 1) + ret[3 * i + 2] = '.'; + } + if (length > 4) + return ret + " (" + to_string(length) + ")"; + return ret; +} +std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 63aa4123ae..f071b4a814 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -8,8 +9,8 @@ #include #include -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "esp32-hal-psram.h" +#ifdef USE_ESP32 +#include #endif #include "esphome/core/optional.h" @@ -19,9 +20,13 @@ #define ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) -#define xSemaphoreWait(semaphore, wait_time) \ - xSemaphoreTake(semaphore, wait_time); \ - xSemaphoreGive(semaphore); +// Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement). +// Define a substitute constexpr keyword for those functions, until we can drop C++11 support. +#if __cplusplus >= 201402L +#define constexpr14 constexpr +#else +#define constexpr14 inline // constexpr implies inline +#endif namespace esphome { @@ -39,19 +44,6 @@ std::string get_mac_address_pretty(); void set_mac_address(uint8_t *mac); #endif -std::string to_string(const std::string &val); -std::string to_string(int val); -std::string to_string(long val); // NOLINT -std::string to_string(long long val); // NOLINT -std::string to_string(unsigned val); // NOLINT -std::string to_string(unsigned long val); // NOLINT -std::string to_string(unsigned long long val); // NOLINT -std::string to_string(float val); -std::string to_string(double val); -std::string to_string(long double val); -optional parse_hex(const std::string &str, size_t start, size_t length); -optional parse_hex(char chr); - /// Compare string a to string b (ignoring case) and return whether they are equal. bool str_equals_case_insensitive(const std::string &a, const std::string &b); bool str_startswith(const std::string &full, const std::string &start); @@ -74,15 +66,6 @@ class HighFrequencyLoopRequester { bool started_{false}; }; -/** Clamp the value between min and max. - * - * @param val The value. - * @param min The minimum value. - * @param max The maximum value. - * @return val clamped in between min and max. - */ -template T clamp(T val, T min, T max); - /** Linearly interpolate between end start and end by completion. * * @tparam T The input/output typename. @@ -103,25 +86,6 @@ template std::unique_ptr make_unique(Args &&... } #endif -/// Return a random 32 bit unsigned integer. -uint32_t random_uint32(); - -/** Returns a random double between 0 and 1. - * - * Note: This function probably doesn't provide a truly uniform distribution. - */ -double random_double(); - -/// Returns a random float between 0 and 1. Essentially just casts random_double() to a float. -float random_float(); - -void fill_random(uint8_t *data, size_t len); - -void fast_random_set_seed(uint32_t seed); -uint32_t fast_random_32(); -uint16_t fast_random_16(); -uint8_t fast_random_8(); - /// Applies gamma correction with the provided gamma to value. float gamma_correct(float value, float gamma); /// Reverts gamma correction with the provided gamma to value. @@ -130,21 +94,16 @@ float gamma_uncorrect(float value, float gamma); /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); -/// Convert a uint64_t to a hex string -std::string uint64_to_string(uint64_t num); - -/// Convert a uint32_t to a hex string -std::string uint32_to_string(uint32_t num); - -uint8_t reverse_bits_8(uint8_t x); -uint16_t reverse_bits_16(uint16_t x); -uint32_t reverse_bits_32(uint32_t x); - /// Convert RGB floats (0-1) to hue (0-360) & saturation/value percentage (0-1) void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value); /// Convert hue (0-360) & saturation/value percentage (0-1) to RGB floats (0-1) void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue); +/// Convert degrees Celsius to degrees Fahrenheit. +static inline float celsius_to_fahrenheit(float value) { return value * 1.8f + 32.0f; } +/// Convert degrees Fahrenheit to degrees Celsius. +static inline float fahrenheit_to_celsius(float value) { return (value - 32.0f) / 1.8f; } + /*** * An interrupt helper class. * @@ -190,10 +149,6 @@ enum ParseOnOffState { ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); -// Encode raw data to a human-readable string (for debugging) -std::string hexencode(const uint8_t *data, uint32_t len); -template std::string hexencode(const T &data) { return hexencode(data.data(), data.size()); } - // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 template struct seq {}; // NOLINT template struct gens : gens {}; // NOLINT @@ -225,17 +180,6 @@ template class CallbackManager { std::vector> callbacks_; }; -// https://stackoverflow.com/a/37161919/8924614 -template -struct is_callable // NOLINT -{ - template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); - - template static auto test(...) -> decltype(std::false_type()); - - static constexpr auto value = decltype(test(nullptr))::value; // NOLINT -}; - void delay_microseconds_safe(uint32_t us); template class Deduplicator { @@ -270,31 +214,104 @@ template class Parented { uint32_t fnv1_hash(const std::string &str); -template T *new_buffer(size_t length) { - T *buffer; -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - if (psramFound()) { - buffer = (T *) ps_malloc(length); - } else { - buffer = new T[length]; // NOLINT(cppcoreguidelines-owning-memory) - } -#else - buffer = new T[length]; // NOLINT(cppcoreguidelines-owning-memory) -#endif - - return buffer; -} - // --------------------------------------------------------------------------------------------------------------------- /// @name STL backports ///@{ -// std::byteswap is from C++23 and technically should be a template, but this will do for now. -constexpr uint8_t byteswap(uint8_t n) { return n; } -constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } -constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } -constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } +// std::to_string() from C++11, available from libstdc++/g++ 8 +// See https://github.com/espressif/esp-idf/issues/1445 +#if _GLIBCXX_RELEASE >= 8 +using std::to_string; +#else +inline std::string to_string(int value) { return str_snprintf("%d", 32, value); } // NOLINT +inline std::string to_string(long value) { return str_snprintf("%ld", 32, value); } // NOLINT +inline std::string to_string(long long value) { return str_snprintf("%lld", 32, value); } // NOLINT +inline std::string to_string(unsigned value) { return str_snprintf("%u", 32, value); } // NOLINT +inline std::string to_string(unsigned long value) { return str_snprintf("%lu", 32, value); } // NOLINT +inline std::string to_string(unsigned long long value) { return str_snprintf("%llu", 32, value); } // NOLINT +inline std::string to_string(float value) { return str_snprintf("%f", 32, value); } +inline std::string to_string(double value) { return str_snprintf("%f", 32, value); } +inline std::string to_string(long double value) { return str_snprintf("%Lf", 32, value); } +#endif + +// std::is_trivially_copyable from C++11, implemented in libstdc++/g++ 5.1 (but minor releases can't be detected) +#if _GLIBCXX_RELEASE >= 6 +using std::is_trivially_copyable; +#else +// Implementing this is impossible without compiler intrinsics, so don't bother. Invalid usage will be detected on +// other variants that use a newer compiler anyway. +// NOLINTNEXTLINE(readability-identifier-naming) +template struct is_trivially_copyable : public std::integral_constant {}; +#endif + +// std::clamp from C++17 +#if __cpp_lib_clamp >= 201603 +using std::clamp; +#else +template constexpr const T &clamp(const T &v, const T &lo, const T &hi, Compare comp) { + return comp(v, lo) ? lo : comp(hi, v) ? hi : v; +} +template constexpr const T &clamp(const T &v, const T &lo, const T &hi) { + return clamp(v, lo, hi, std::less{}); +} +#endif + +// std::is_invocable from C++17 +#if __cpp_lib_is_invocable >= 201703 +using std::is_invocable; +#else +// https://stackoverflow.com/a/37161919/8924614 +template struct is_invocable { // NOLINT(readability-identifier-naming) + template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); + template static auto test(...) -> decltype(std::false_type()); + static constexpr auto value = decltype(test(nullptr))::value; // NOLINT +}; +#endif + +// std::bit_cast from C++20 +#if __cpp_lib_bit_cast >= 201806 +using std::bit_cast; +#else +/// Convert data between types, without aliasing issues or undefined behaviour. +template< + typename To, typename From, + enable_if_t::value && is_trivially_copyable::value, + int> = 0> +To bit_cast(const From &src) { + To dst; + memcpy(&dst, &src, sizeof(To)); + return dst; +} +#endif + +// std::byteswap from C++23 +template constexpr14 T byteswap(T n) { + T m; + for (size_t i = 0; i < sizeof(T); i++) + reinterpret_cast(&m)[i] = reinterpret_cast(&n)[sizeof(T) - 1 - i]; + return m; +} +template<> constexpr14 uint8_t byteswap(uint8_t n) { return n; } +template<> constexpr14 uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } +template<> constexpr14 uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } +template<> constexpr14 uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } +template<> constexpr14 int8_t byteswap(int8_t n) { return n; } +template<> constexpr14 int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } +template<> constexpr14 int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } +template<> constexpr14 int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } + +///@} + +/// @name Mathematics +///@{ + +/// Return a random 32-bit unsigned integer. +uint32_t random_uint32(); +/// Return a random float between 0 and 1. +float random_float(); +/// Generate \p len number of random bytes. +void random_bytes(uint8_t *data, size_t len); ///@} @@ -312,7 +329,8 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui } /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). -template::value, int> = 0> inline T encode_value(const uint8_t *bytes) { +template::value, int> = 0> +constexpr14 T encode_value(const uint8_t *bytes) { T val = 0; for (size_t i = 0; i < sizeof(T); i++) { val <<= 8; @@ -322,12 +340,12 @@ template::value, int> = 0> inline T } /// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T). template::value, int> = 0> -inline T encode_value(const std::array bytes) { +constexpr14 T encode_value(const std::array bytes) { return encode_value(bytes.data()); } /// Decode a value into its constituent bytes (from most to least significant). template::value, int> = 0> -inline std::array decode_value(T val) { +constexpr14 std::array decode_value(T val) { std::array ret{}; for (size_t i = sizeof(T); i > 0; i--) { ret[i - 1] = val & 0xFF; @@ -336,8 +354,25 @@ inline std::array decode_value(T val) { return ret; } +/// Reverse the order of 8 bits. +inline uint8_t reverse_bits(uint8_t x) { + x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); + x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); + x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4); + return x; +} +/// Reverse the order of 16 bits. +inline uint16_t reverse_bits(uint16_t x) { + return (reverse_bits(static_cast(x & 0xFF)) << 8) | reverse_bits(static_cast((x >> 8) & 0xFF)); +} +/// Reverse the order of 32 bits. +inline uint32_t reverse_bits(uint32_t x) { + return (reverse_bits(static_cast(x & 0xFFFF)) << 16) | + reverse_bits(static_cast((x >> 16) & 0xFFFF)); +} + /// Convert a value between host byte order and big endian (most significant byte first) order. -template::value, int> = 0> constexpr T convert_big_endian(T val) { +template constexpr14 T convert_big_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return byteswap(val); #else @@ -345,11 +380,23 @@ template::value, int> = 0> constexpr #endif } +/// Convert a value between host byte order and little endian (least significant byte first) order. +template constexpr14 T convert_little_endian(T val) { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + return val; +#else + return byteswap(val); +#endif +} + ///@} /// @name Strings ///@{ +/// Convert the value to a string (added as extra overload so that to_string() can be used on all stringifiable types). +inline std::string to_string(const std::string &val) { return val; } + /// Truncate a string to a specific length. std::string str_truncate(const std::string &str, size_t length); @@ -359,6 +406,10 @@ std::string str_until(const char *str, char ch); /// Extract the part of the string until either the first occurence of the specified character, or the end. std::string str_until(const std::string &str, char ch); +/// Convert the string to lower case. +std::string str_lower_case(const std::string &str); +/// Convert the string to upper case. +std::string str_upper_case(const std::string &str); /// Convert the string to snake case (lowercase with underscores). std::string str_snake_case(const std::string &str); @@ -370,9 +421,9 @@ std::string str_sanitize(const std::string &str); /// @name Parsing & formatting ///@{ -/// Parse an unsigned decimal number (requires null-terminated string). +/// Parse an unsigned decimal number from a null-terminated string. template::value && std::is_unsigned::value), int> = 0> -optional parse_number(const char *str, size_t len) { +optional parse_number(const char *str) { char *end = nullptr; unsigned long value = ::strtoul(str, &end, 10); // NOLINT(google-runtime-int) if (end == str || *end != '\0' || value > std::numeric_limits::max()) @@ -382,11 +433,11 @@ optional parse_number(const char *str, size_t len) { /// Parse an unsigned decimal number. template::value && std::is_unsigned::value), int> = 0> optional parse_number(const std::string &str) { - return parse_number(str.c_str(), str.length() + 1); + return parse_number(str.c_str()); } -/// Parse a signed decimal number (requires null-terminated string). +/// Parse a signed decimal number from a null-terminated string. template::value && std::is_signed::value), int> = 0> -optional parse_number(const char *str, size_t len) { +optional parse_number(const char *str) { char *end = nullptr; signed long value = ::strtol(str, &end, 10); // NOLINT(google-runtime-int) if (end == str || *end != '\0' || value < std::numeric_limits::min() || value > std::numeric_limits::max()) @@ -396,11 +447,10 @@ optional parse_number(const char *str, size_t len) { /// Parse a signed decimal number. template::value && std::is_signed::value), int> = 0> optional parse_number(const std::string &str) { - return parse_number(str.c_str(), str.length() + 1); + return parse_number(str.c_str()); } -/// Parse a decimal floating-point number (requires null-terminated string). -template::value), int> = 0> -optional parse_number(const char *str, size_t len) { +/// Parse a decimal floating-point number from a null-terminated string. +template::value), int> = 0> optional parse_number(const char *str) { char *end = nullptr; float value = ::strtof(str, &end); if (end == str || *end != '\0' || value == HUGE_VALF) @@ -410,7 +460,147 @@ optional parse_number(const char *str, size_t len) { /// Parse a decimal floating-point number. template::value), int> = 0> optional parse_number(const std::string &str) { - return parse_number(str.c_str(), str.length() + 1); + return parse_number(str.c_str()); +} + +/** Parse bytes from a hex-encoded string into a byte array. + * + * When \p len is less than \p 2*count, the result is written to the back of \p data (i.e. this function treats \p str + * as if it were padded with zeros at the front). + * + * @param str String to read from. + * @param len Length of \p str (excluding optional null-terminator), is a limit on the number of characters parsed. + * @param data Byte array to write to. + * @param count Length of \p data. + * @return The number of characters parsed from \p str. + */ +size_t parse_hex(const char *str, size_t len, uint8_t *data, size_t count); +/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into array \p data. +inline bool parse_hex(const char *str, uint8_t *data, size_t count) { + return parse_hex(str, strlen(str), data, count) == 2 * count; +} +/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into array \p data. +inline bool parse_hex(const std::string &str, uint8_t *data, size_t count) { + return parse_hex(str.c_str(), str.length(), data, count) == 2 * count; +} +/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into vector \p data. +inline bool parse_hex(const char *str, std::vector &data, size_t count) { + data.resize(count); + return parse_hex(str, strlen(str), data.data(), count) == 2 * count; +} +/// Parse \p count bytes from the hex-encoded string \p str of at least \p 2*count characters into vector \p data. +inline bool parse_hex(const std::string &str, std::vector &data, size_t count) { + data.resize(count); + return parse_hex(str.c_str(), str.length(), data.data(), count) == 2 * count; +} +/** Parse a hex-encoded string into an unsigned integer. + * + * @param str String to read from, starting with the most significant byte. + * @param len Length of \p str (excluding optional null-terminator), is a limit on the number of characters parsed. + */ +template::value, int> = 0> +optional parse_hex(const char *str, size_t len) { + T val = 0; + if (len > 2 * sizeof(T) || parse_hex(str, len, reinterpret_cast(&val), sizeof(T)) == 0) + return {}; + return convert_big_endian(val); +} +/// Parse a hex-encoded null-terminated string (starting with the most significant byte) into an unsigned integer. +template::value, int> = 0> optional parse_hex(const char *str) { + return parse_hex(str, strlen(str)); +} +/// Parse a hex-encoded null-terminated string (starting with the most significant byte) into an unsigned integer. +template::value, int> = 0> optional parse_hex(const std::string &str) { + return parse_hex(str.c_str(), str.length()); +} + +/// Format the byte array \p data of length \p len in lowercased hex. +std::string format_hex(const uint8_t *data, size_t length); +/// Format the vector \p data in lowercased hex. +std::string format_hex(const std::vector &data); +/// Format an unsigned integer in lowercased hex, starting with the most significant byte. +template::value, int> = 0> std::string format_hex(T val) { + val = convert_big_endian(val); + return format_hex(reinterpret_cast(&val), sizeof(T)); +} + +/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex. +std::string format_hex_pretty(const uint8_t *data, size_t length); +/// Format the vector \p data in pretty-printed, human-readable hex. +std::string format_hex_pretty(const std::vector &data); +/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. +template::value, int> = 0> std::string format_hex_pretty(T val) { + val = convert_big_endian(val); + return format_hex_pretty(reinterpret_cast(&val), sizeof(T)); +} + +///@} + +/// @name Number manipulation +///@{ + +/// Remap a number from one range to another. +template constexpr T remap(U value, U min, U max, T min_out, T max_out) { + return (value - min) * (max_out - min_out) / (max - min) + min_out; +} + +///@} + +/// @name Memory management +///@{ + +/** An STL allocator that uses SPI RAM. + * + * By setting flags, it can be configured to don't try main memory if SPI RAM is full or unavailable, and to return + * `nulllptr` instead of aborting when no memory is available. + */ +template class ExternalRAMAllocator { + public: + using value_type = T; + + enum Flags { + NONE = 0, + REFUSE_INTERNAL = 1 << 0, ///< Refuse falling back to internal memory when external RAM is full or unavailable. + ALLOW_FAILURE = 1 << 1, ///< Don't abort when memory allocation fails. + }; + + ExternalRAMAllocator() = default; + ExternalRAMAllocator(Flags flags) : flags_{flags} {} + template constexpr ExternalRAMAllocator(const ExternalRAMAllocator &other) : flags_{other.flags} {} + + T *allocate(size_t n) { + size_t size = n * sizeof(T); + T *ptr = nullptr; +#ifdef USE_ESP32 + ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)); +#endif + if (ptr == nullptr && (this->flags_ & Flags::REFUSE_INTERNAL) == 0) + ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + if (ptr == nullptr && (this->flags_ & Flags::ALLOW_FAILURE) == 0) + abort(); + return ptr; + } + + void deallocate(T *p, size_t n) { + free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + } + + private: + Flags flags_{Flags::NONE}; +}; + +/// @} + +/// @name Deprecated functions +///@{ + +ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1") +inline std::string hexencode(const uint8_t *data, uint32_t len) { return format_hex_pretty(data, len); } + +template +ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1") +std::string hexencode(const T &data) { + return hexencode(data.data(), data.size()); } ///@} diff --git a/esphome/core/log.h b/esphome/core/log.h index 590ad26032..1e93ed4219 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -33,7 +33,7 @@ namespace esphome { #define ESPHOME_LOG_LEVEL_VERY_VERBOSE 7 #ifndef ESPHOME_LOG_LEVEL -#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG +#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_NONE #endif #define ESPHOME_LOG_COLOR_BLACK "30" diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index ad45cd9684..2b13061a59 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -2,7 +2,8 @@ #include #include -#include + +#include "esphome/core/helpers.h" namespace esphome { @@ -45,20 +46,12 @@ class ESPPreferences { */ virtual bool sync() = 0; -#ifndef USE_ESP8266 - template::value, bool>::type = true> -#else - // esp8266 toolchain doesn't have is_trivially_copyable - template -#endif + template::value, bool> = true> ESPPreferenceObject make_preference(uint32_t type, bool in_flash) { return this->make_preference(sizeof(T), type, in_flash); } -#ifndef USE_ESP8266 - template::value, bool>::type = true> -#else - template -#endif + + template::value, bool> = true> ESPPreferenceObject make_preference(uint32_t type) { return this->make_preference(sizeof(T), type); } diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 13d088e1cb..806a2d832c 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -27,8 +27,7 @@ Application = esphome_ns.class_("Application") optional = esphome_ns.class_("optional") arduino_json_ns = global_ns.namespace("ArduinoJson") JsonObject = arduino_json_ns.class_("JsonObject") -JsonObjectRef = JsonObject.operator("ref") -JsonObjectConstRef = JsonObjectRef.operator("const") +JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") Controller = esphome_ns.class_("Controller") GPIOPin = esphome_ns.class_("GPIOPin") InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 5e5cc4ecd2..ca257d93b4 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -410,6 +410,17 @@ class DownloadBinaryRequestHandler(BaseHandler): filename = f"{storage_json.name}.bin" path = storage_json.firmware_bin_path + elif type == "firmware-factory.bin": + storage_path = ext_storage_path(settings.config_dir, configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + filename = f"{storage_json.name}.bin" + path = storage_json.firmware_bin_path.replace( + "firmware.bin", "firmware-factory.bin" + ) + else: args = ["esphome", "idedata", settings.rel_path(configuration)] rc, stdout, _ = run_system_command(*args) @@ -601,7 +612,7 @@ class MainRequestHandler(BaseHandler): begin = bool(self.get_argument("begin", False)) self.render( - get_template_path("index"), + "index.template.html", begin=begin, **template_args(), login_enabled=settings.using_password, @@ -778,7 +789,7 @@ class LoginHandler(BaseHandler): def render_login_page(self, error=None): self.render( - get_template_path("login"), + "login.template.html", error=error, hassio=settings.using_hassio_auth, has_username=bool(settings.username), @@ -872,10 +883,6 @@ def get_base_frontend_path(): return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) -def get_template_path(template_name): - return os.path.join(get_base_frontend_path(), f"{template_name}.template.html") - - def get_static_path(*args): return os.path.join(get_base_frontend_path(), "static", *args) @@ -933,6 +940,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): "cookie_secret": settings.cookie_secret, "log_function": log_function, "websocket_ping_interval": 30.0, + "template_path": get_base_frontend_path(), } rel = settings.relative_url app = tornado.web.Application( diff --git a/esphome/wizard.py b/esphome/wizard.py index f2632caf71..c64ad3a583 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -160,7 +160,6 @@ if get_bool_env(ENV_QUICKWIZARD): def sleep(time): pass - else: from time import sleep diff --git a/esphome/writer.py b/esphome/writer.py index 8963572752..89a074683a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -290,6 +290,11 @@ def copy_src_tree(): copy_files() + elif CORE.is_esp8266: + from esphome.components.esp8266 import copy_files + + copy_files() + def generate_defines_h(): define_content_l = [x.as_macro for x in CORE.defines] diff --git a/platformio.ini b/platformio.ini index 3c0b725d65..589624a71d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,18 +4,23 @@ ; It's *not* used during runtime. [platformio] -default_envs = esp8266, esp32, esp32-idf +default_envs = esp8266-arduino, esp32-arduino, esp32-idf +; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build +; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this +; being the root directory. Instead, set esphome/ as the source directory, all our sources are in +; there anyway. Set the root directory as the include_dir, so that the esphome/ directory is on the +; include path. src_dir = esphome -include_dir = +include_dir = . -[runtime] -; This are the flags as set by the runtime. +; This are just the build flags as set by the runtime. +[flags:runtime] build_flags = -Wno-unused-but-set-variable -Wno-sign-compare -[clangtidy] -; This are the flags for clang-tidy. +; This are just the build flags for clang-tidy. +[flags:clangtidy] build_flags = -Wall -Wextra @@ -25,11 +30,13 @@ build_flags = -Wshadow-field-in-constructor -Wshadow-uncaptured-local +; This are common settings for all environments. [common] lib_deps = - esphome/noise-c@0.1.4 ; api - makuna/NeoPixelBus@2.6.9 ; neopixelbus - esphome/Improv@1.0.0 ; improv_serial / esp32_improv + esphome/noise-c@0.1.4 ; api + makuna/NeoPixelBus@2.6.9 ; neopixelbus + esphome/Improv@1.0.0 ; improv_serial / esp32_improv + bblanchon/ArduinoJson@6.18.5 ; json build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = @@ -37,12 +44,12 @@ src_filter = +<../tests/dummy_main.cpp> +<../.temp/all-include.cpp> +; This are common settings for all Arduino-framework based environments. [common:arduino] extends = common lib_deps = ${common.lib_deps} ottowinter/AsyncMqttClient-esphome@0.8.6 ; mqtt - ottowinter/ArduinoJson-esphomelib@5.13.3 ; json esphome/ESPAsyncWebServer-esphome@2.1.0 ; web_server_base fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps @@ -57,13 +64,15 @@ build_flags = ${common.build_flags} -DUSE_ARDUINO +; This are common settings for all IDF-framework based environments. [common:idf] extends = common build_flags = ${common.build_flags} -DUSE_ESP_IDF -[common:esp8266] +; This are common settings for the ESP8266 using Arduino. +[common:esp8266-arduino] extends = common:arduino ; when changing this also copy it to esphome-docker-base images platform = platformio/espressif8266 @ 3.2.0 @@ -71,7 +80,6 @@ platform_packages = platformio/framework-arduinoespressif8266 @ ~3.30002.0 framework = arduino -board = nodemcuv2 lib_deps = ${common:arduino.lib_deps} ESP8266WiFi ; wifi (Arduino built-in) @@ -81,7 +89,9 @@ build_flags = ${common:arduino.build_flags} -DUSE_ESP8266 -DUSE_ESP8266_FRAMEWORK_ARDUINO +extra_scripts = post:esphome/components/esp8266/post_build.py +; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino ; when changing this also copy it to esphome-docker-base images @@ -98,7 +108,9 @@ build_flags = ${common:arduino.build_flags} -DUSE_ESP32 -DUSE_ESP32_FRAMEWORK_ARDUINO +extra_scripts = post:esphome/components/esp32/post_build.py +; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf ; when changing this also copy it to esphome-docker-base images @@ -107,7 +119,6 @@ platform_packages = platformio/framework-espidf @ ~3.40300.0 framework = espidf -board = nodemcu-32s lib_deps = ${common:idf.lib_deps} espressif/esp32-camera@1.0.0 ; esp32_camera @@ -116,41 +127,81 @@ build_flags = -Wno-nonnull-compare -DUSE_ESP32 -DUSE_ESP32_FRAMEWORK_ESP_IDF +extra_scripts = post:esphome/components/esp32/post_build.py -[env:esp8266] -extends = common:esp8266 +; All the actual environments are defined below. +[env:esp8266-arduino] +extends = common:esp8266-arduino +board = nodemcuv2 build_flags = - ${common:esp8266.build_flags} - ${runtime.build_flags} + ${common:esp8266-arduino.build_flags} + ${flags:runtime.build_flags} -[env:esp8266-tidy] -extends = common:esp8266 +[env:esp8266-arduino-tidy] +extends = common:esp8266-arduino +board = nodemcuv2 build_flags = - ${common:esp8266.build_flags} - ${clangtidy.build_flags} + ${common:esp8266-arduino.build_flags} + ${flags:clangtidy.build_flags} -[env:esp32] +[env:esp32-arduino] extends = common:esp32-arduino +board = esp32dev build_flags = ${common:esp32-arduino.build_flags} - ${runtime.build_flags} + ${flags:runtime.build_flags} -[env:esp32-tidy] +[env:esp32-arduino-tidy] extends = common:esp32-arduino +board = esp32dev build_flags = ${common:esp32-arduino.build_flags} - ${clangtidy.build_flags} + ${flags:clangtidy.build_flags} [env:esp32-idf] extends = common:esp32-idf +board = esp32dev board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf build_flags = ${common:esp32-idf.build_flags} - ${runtime.build_flags} + ${flags:runtime.build_flags} [env:esp32-idf-tidy] extends = common:esp32-idf +board = esp32dev board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf-tidy build_flags = ${common:esp32-idf.build_flags} - ${clangtidy.build_flags} + ${flags:clangtidy.build_flags} + +[env:esp32c3-idf] +extends = common:esp32-idf +board = esp32-c3-devkitm-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + +[env:esp32c3-idf-tidy] +extends = common:esp32-idf +board = esp32-c3-devkitm-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf-tidy +build_flags = + ${common:esp32-idf.build_flags} + ${flags:clangtidy.build_flags} + +[env:esp32s2-idf] +extends = common:esp32-idf +board = esp32-s2-kaluga-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + +[env:esp32s2-idf-tidy] +extends = common:esp32-idf +board = esp32-s2-kaluga-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf-tidy +build_flags = + ${common:esp32-idf.build_flags} + ${flags:clangtidy.build_flags} diff --git a/requirements.txt b/requirements.txt index c45797a71f..b21218f511 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,12 @@ tornado==6.1 tzlocal==4.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==5.2.2 # When updating platformio, also update Dockerfile +platformio==5.2.4 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20211211.0 +esphome-dashboard==20220113.1 aioesphomeapi==10.6.0 -zeroconf==0.36.13 +zeroconf==0.37.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/requirements_test.txt b/requirements_test.txt index b916e8bb1b..4d5c40296f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ -pylint==2.12.1 +pylint==2.12.2 flake8==4.0.1 -black==21.11b1 +black==21.12b0 pre-commit # Unit tests diff --git a/script/clang-tidy b/script/clang-tidy index 7450084634..8a7d229887 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from helpers import print_error_for_file, get_output, filter_grep, \ - build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, basepath + build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, root_path, basepath import argparse import click import colorama @@ -17,9 +17,19 @@ import threading def clang_options(idedata): - cmd = [ - # target 32-bit arch (this prevents size mismatch errors on a 64-bit host) - '-m32', + cmd = [] + + # extract target architecture from triplet in g++ filename + triplet = os.path.basename(idedata['cxx_path'])[:-4] + if triplet.startswith("xtensa-"): + # clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler + cmd.append('-m32') + cmd.append('-D__XTENSA__') + else: + cmd.append(f'--target={triplet}') + + # set flags + cmd.extend([ # disable built-in include directories from the host '-nostdinc', '-nostdinc++', @@ -39,15 +49,13 @@ def clang_options(idedata): # suppress warning about attribute cannot be applied to type # https://github.com/esp8266/Arduino/pull/8258 '-Ddeprecated(x)=', - # pretend we're an Xtensa compiler, which gates some features in the headers - '-D__XTENSA__', # allow to condition code on the presence of clang-tidy '-DCLANG_TIDY', # (esp-idf) Disable this header because they use asm with registers clang-tidy doesn't know '-D__XTENSA_API_H__', # (esp-idf) Fix __once_callable in some libstdc++ headers '-D_GLIBCXX_HAVE_TLS', - ] + ]) # copy compiler flags, except those clang doesn't understand. cmd.extend(flag for flag in idedata['cxx_flags'].split(' ') @@ -58,13 +66,21 @@ def clang_options(idedata): # defines cmd.extend(f'-D{define}' for define in idedata['defines']) - # add include directories, using -isystem for dependencies to suppress their errors + # add toolchain include directories using -isystem to suppress their errors + # idedata contains include directories for all toolchains of this platform, only use those from the one in use + toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../") for directory in idedata['includes']['toolchain']: - if 'xtensa-esp32s2-elf' not in directory: + if directory.startswith(toolchain_dir): cmd.extend(['-isystem', directory]) + + # add library include directories using -isystem to suppress their errors for directory in sorted(set(idedata['includes']['build'])): - dependency = "framework-arduino" in directory or "/libdeps/" in directory - cmd.extend(['-isystem' if dependency else '-I', directory]) + # skip our own directories, we add those later + if not directory.startswith(f"{root_path}/") or directory.startswith(f"{root_path}/.pio/"): + cmd.extend(['-isystem', directory]) + + # add the esphome include directory using -I + cmd.extend(['-I', root_path]) return cmd @@ -118,8 +134,8 @@ def main(): parser.add_argument('-j', '--jobs', type=int, default=multiprocessing.cpu_count(), help='number of tidy instances to be run in parallel.') - parser.add_argument('-e', '--environment', default='esp32-tidy', - help='the PlatformIO environment to run against (esp8266-tidy or esp32-tidy)') + parser.add_argument('-e', '--environment', default='esp32-arduino-tidy', + help='the PlatformIO environment to use (as defined in platformio.ini)') parser.add_argument('files', nargs='*', default=[], help='files to be processed (regex on path)') parser.add_argument('--fix', action='store_true', help='apply fix-its') diff --git a/script/setup b/script/setup index 6d095af46c..71828deeaa 100755 --- a/script/setup +++ b/script/setup @@ -5,6 +5,6 @@ set -e cd "$(dirname "$0")/.." pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -pip3 install -e . +pip3 install --no-use-pep517 -e . pre-commit install diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py new file mode 100644 index 0000000000..690d323a50 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -0,0 +1,52 @@ +"""Tests for the deep sleep component.""" + + +def test_deep_sleep_setup(generate_main): + """ + When the deep sleep is set in the yaml file, it should be registered in main + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" + ) + + assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp + assert "App.register_component(deepsleep);" in main_cpp + + +def test_deep_sleep_sleep_duration(generate_main): + """ + When deep sleep is configured with sleep duration, it should be set. + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" + ) + + assert "deepsleep->set_sleep_duration(60000);" in main_cpp + + +def test_deep_sleep_run_duration_simple(generate_main): + """ + When deep sleep is configured with run duration, it should be set. + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" + ) + + assert "deepsleep->set_run_duration(10000);" in main_cpp + + +def test_deep_sleep_run_duration_dictionary(generate_main): + """ + When deep sleep is configured with dictionary run duration, it should be set. + """ + main_cpp = generate_main( + "tests/component_tests/deep_sleep/test_deep_sleep2.yaml" + ) + + assert ( + "deepsleep->set_run_duration(deep_sleep::WakeupCauseToRunDuration{\n" + " .default_cause = 10000,\n" + " .touch_cause = 10000,\n" + " .gpio_cause = 30000,\n" + "});" + ) in main_cpp diff --git a/tests/component_tests/deep_sleep/test_deep_sleep1.yaml b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml new file mode 100644 index 0000000000..18a425df58 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + platform: ESP32 + board: nodemcu-32s + +deep_sleep: + id: deepsleep + sleep_duration: 1min + run_duration: 10s diff --git a/tests/component_tests/deep_sleep/test_deep_sleep2.yaml b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml new file mode 100644 index 0000000000..49a7f510f2 --- /dev/null +++ b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + platform: ESP32 + board: nodemcu-32s + +deep_sleep: + id: deepsleep + sleep_duration: 1min + run_duration: + default: 10s + gpio_wakeup_reason: 30s diff --git a/tests/test1.yaml b/tests/test1.yaml index 18c6610b08..d3351e3b12 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -657,6 +657,15 @@ sensor: name: 'INA3221 Channel 1 Shunt Voltage' update_interval: 15s i2c_id: i2c_bus + - platform: kalman_combinator + name: "Kalman-filtered temperature" + process_std_dev: 0.00139 + sources: + - source: scd30_temperature + error: !lambda |- + return 0.4 + std::abs(x - 25) * 0.023; + - source: scd4x_temperature + error: 1.5 - platform: htu21d temperature: name: 'Living Room Temperature 6' @@ -820,6 +829,7 @@ sensor: co2: name: 'Living Room CO2 9' temperature: + id: scd30_temperature name: 'Living Room Temperature 9' humidity: name: 'Living Room Humidity 9' @@ -834,6 +844,7 @@ sensor: co2: name: "SCD4X CO2" temperature: + id: scd4x_temperature name: "SCD4X Temperature" humidity: name: "SCD4X Humidity" @@ -1707,6 +1718,9 @@ climate: name: HeatpumpIR Climate min_temperature: 18 max_temperature: 30 + - platform: midea_ir + name: Midea IR + use_fahrenheit: true - platform: midea on_state: logger.log: "State changed!" @@ -2119,6 +2133,8 @@ display: mcp23xxx: mcp23017_hub number: 2 intensity: 3 + inverted: true + length: 4 lambda: |- it.print("1234"); - platform: pcd8544 @@ -2456,6 +2472,11 @@ text_sensor: id: glob_int value: '0' - canbus.send: + canbus_id: mcp2515_can + can_id: 23 + data: [0x10, 0x20, 0x30] + - canbus.send: + canbus_id: esp32_internal_can can_id: 23 data: [0x10, 0x20, 0x30] - platform: template @@ -2493,6 +2514,7 @@ rtttl: canbus: - platform: mcp2515 + id: mcp2515_can cs_pin: GPIO17 can_id: 4 bit_rate: 50kbps @@ -2509,6 +2531,25 @@ canbus: lambda: 'return x[0] == 0x11;' then: light.toggle: ${roomname}_lights + - platform: esp32_can + id: esp32_internal_can + rx_pin: GPIO04 + tx_pin: GPIO05 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", &b[0] ); + - can_id: 23 + then: + - if: + condition: + lambda: 'return x[0] == 0x11;' + then: + light.toggle: ${roomname}_lights teleinfo: id: myteleinfo diff --git a/tests/test2.yaml b/tests/test2.yaml index 50743dc643..7920bf3fe3 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -55,7 +55,10 @@ logger: level: DEBUG deep_sleep: - run_duration: 20s + run_duration: + default: 20s + gpio_wakeup_reason: 10s + touch_wakeup_reason: 15s sleep_duration: 50s wakeup_pin: GPIO39 wakeup_pin_mode: INVERT_WAKEUP @@ -304,6 +307,15 @@ sensor: name: "Wave Mini Pressure" tvoc: name: "Wave Mini VOC" + - platform: ina260 + address: 0x40 + current: + name: "INA260 Current" + power: + name: "INA260 Power" + bus_voltage: + name: "INA260 Voltage" + update_interval: 60s time: - platform: homeassistant diff --git a/tests/test3.yaml b/tests/test3.yaml index 50cd6d6cf6..607d985704 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -358,6 +358,11 @@ sensor: - filter_out: NAN - sliding_window_moving_average: - exponential_moving_average: + - quantile: + window_size: 5 + send_every: 5 + send_first_at: 3 + quantile: .8 - lambda: 'return 0;' - delta: 100 - throttle: 100ms @@ -373,6 +378,10 @@ sensor: - 400 -> 500 - -50 -> -1000 - -100 -> -10000 + - platform: cd74hc4067 + id: cd74hc4067_0 + number: 0 + sensor: my_sensor - platform: resistance sensor: my_sensor configuration: DOWNSTREAM @@ -450,15 +459,30 @@ sensor: active_power_b: name: ADE7953 Active Power B id: ade7953_active_power_b + - platform: bl0940 + uart_id: uart3 + voltage: + name: 'BL0940 Voltage' + current: + name: 'BL0940 Current' + power: + name: 'BL0940 Power' + energy: + name: 'BL0940 Energy' + internal_temperature: + name: 'BL0940 Internal temperature' + external_temperature: + name: 'BL0940 External temperature' - platform: pzem004t uart_id: uart3 voltage: - name: 'PZEM00T Voltage' + name: 'PZEM004T Voltage' current: name: 'PZEM004T Current' power: name: 'PZEM004T Power' - platform: pzemac + id: pzemac1 voltage: name: 'PZEMAC Voltage' current: @@ -757,6 +781,11 @@ binary_sensor: on_press: then: - cover.toggle: time_based_cover + - platform: template + id: 'pzemac_reset_energy' + on_press: + then: + - pzemac.reset_energy: pzemac1 globals: - id: my_global_string @@ -1320,3 +1349,9 @@ dsmr: daly_bms: update_interval: 20s uart_id: uart1 + +cd74hc4067: + pin_s0: GPIO12 + pin_s1: GPIO13 + pin_s2: GPIO14 + pin_s3: GPIO15 diff --git a/tests/test4.yaml b/tests/test4.yaml index b4708acf65..eec1c2eb5e 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -65,6 +65,9 @@ sx1509: - id: sx1509_hub address: 0x3E +mcp3204: + cs_pin: GPIO23 + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -215,6 +218,9 @@ sensor: - or: - throttle: "20min" - delta: 0.02 + - platform: mcp3204 + name: "MCP3204 Pin 1" + number: 1 # # platform sensor.apds9960 requires component apds9960 @@ -413,6 +419,7 @@ display: reset_pin: GPIO23 model: 2.90in full_update_every: 30 + reset_duration: 200ms lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper @@ -440,7 +447,13 @@ display: wakeup_pin: GPIO1 vcom_pin: GPIO1 - +number: + - platform: tuya + id: tuya_number + number_datapoint: 102 + min_value: 0 + max_value: 17 + step: 1 text_sensor: - platform: pipsolar @@ -524,3 +537,7 @@ xpt2046: button: - platform: restart name: Restart Button + - platform: safe_mode + name: Safe Mode Button + - platform: shutdown + name: Shutdown Button diff --git a/tests/test5.yaml b/tests/test5.yaml index 37e65e7da2..d6acbf1e65 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -72,6 +72,9 @@ output: channel: 0 max_power: 0.8 + - platform: mcp47a1 + id: output_mcp47a1 + demo: esp32_ble: @@ -180,6 +183,15 @@ sensor: co2: name: CO2 Sensor + - platform: bmp3xx + temperature: + name: "BMP Temperature" + oversampling: 16x + pressure: + name: "BMP Pressure" + address: 0x77 + iir_filter: 2X + script: - id: automation_test then: diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 32d82b3062..3f32a117ff 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -68,8 +68,7 @@ from esphome import codegen as cg "optional", "arduino_json_ns", "JsonObject", - "JsonObjectRef", - "JsonObjectConstRef", + "JsonObjectConst", "Controller", "GPIOPin", ),