diff --git a/.clang-tidy b/.clang-tidy index c9b77b57201b..946f2950d8fe 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,9 +5,12 @@ Checks: >- -altera-*, -android-*, -boost-*, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, -bugprone-narrowing-conversions, -bugprone-signed-char-misuse, -cert-dcl50-cpp, + -cert-err33-c, -cert-err58-cpp, -cert-oop57-cpp, -cert-str34-c, @@ -15,6 +18,7 @@ Checks: >- -clang-analyzer-osx.*, -clang-diagnostic-delete-abstract-non-virtual-dtor, -clang-diagnostic-delete-non-abstract-non-virtual-dtor, + -clang-diagnostic-ignored-optimization-argument, -clang-diagnostic-shadow-field, -clang-diagnostic-unused-const-variable, -clang-diagnostic-unused-parameter, @@ -25,6 +29,7 @@ Checks: >- -cppcoreguidelines-macro-usage, -cppcoreguidelines-narrowing-conversions, -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-prefer-member-initializer, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-pointer-arithmetic, @@ -36,6 +41,7 @@ Checks: >- -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-special-member-functions, + -cppcoreguidelines-virtual-class-destructor, -fuchsia-multiple-inheritance, -fuchsia-overloaded-operator, -fuchsia-statically-constructed-objects, @@ -68,6 +74,7 @@ Checks: >- -modernize-use-nodiscard, -mpi-*, -objc-*, + -readability-container-data-pointer, -readability-convert-member-functions-to-static, -readability-else-after-return, -readability-function-cognitive-complexity, @@ -82,8 +89,6 @@ WarningsAsErrors: '*' AnalyzeTemporaryDtors: false FormatStyle: google CheckOptions: - - key: google-readability-braces-around-statements.ShortStatementLines - value: '1' - key: google-readability-function-size.StatementThreshold value: '800' - key: google-runtime-int.TypeSuffix @@ -158,3 +163,9 @@ CheckOptions: value: '' - key: readability-qualified-auto.AddConstToQualified value: 0 + - key: readability-identifier-length.MinimumVariableNameLength + value: 0 + - key: readability-identifier-length.MinimumParameterNameLength + value: 0 + - key: readability-identifier-length.MinimumLoopCounterNameLength + value: 0 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7e5a0c19c9df..ab4f8cc9609e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,53 +4,61 @@ "postCreateCommand": [ "script/devcontainer-post-create" ], + "containerEnv": { + "DEVCONTAINER": "1" + }, "runArgs": [ "--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1" ], "appPort": 6052, - "extensions": [ - // python - "ms-python.python", - "visualstudioexptteam.vscodeintellicode", - // yaml - "redhat.vscode-yaml", - // cpp - "ms-vscode.cpptools", - // editorconfig - "editorconfig.editorconfig", - ], - "settings": { - "python.languageServer": "Pylance", - "python.pythonPath": "/usr/bin/python3", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.defaultProfile.linux": "bash", - "yaml.customTags": [ - "!secret scalar", - "!lambda scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ], - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": { - "when": "$(basename).py" - }, - "**/__pycache__": true - }, - "files.associations": { - "**/.vscode/*.json": "jsonc" - }, - "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", + "customizations": { + "vscode": { + "extensions": [ + // python + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + // yaml + "redhat.vscode-yaml", + // cpp + "ms-vscode.cpptools", + // editorconfig + "editorconfig.editorconfig", + ], + "settings": { + "python.languageServer": "Pylance", + "python.pythonPath": "/usr/bin/python3", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.defaultProfile.linux": "bash", + "yaml.customTags": [ + "!secret scalar", + "!lambda scalar", + "!extend scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": { + "when": "$(basename).py" + }, + "**/__pycache__": true + }, + "files.associations": { + "**/.vscode/*.json": "jsonc" + }, + "C_Cpp.clang_format_path": "/usr/bin/clang-format-13" + } + } } } diff --git a/.gitattributes b/.gitattributes index dad096622283..1b3fd332b4f8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Normalize line endings to LF in the repository * text eol=lf +*.png binary diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3221b8ac5c3d..3bf9c4e1f6f2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,6 +19,8 @@ - [ ] ESP32 IDF - [ ] ESP8266 - [ ] RP2040 +- [ ] BK72xx +- [ ] RTL87xx ## Example entry for `config.yaml`: API Reference --> Two-Wire Automotive Interface (TWAI) + +CAN_SPEEDS_ESP32 = { + "25KBPS": CanSpeed.CAN_25KBPS, "50KBPS": CanSpeed.CAN_50KBPS, "100KBPS": CanSpeed.CAN_100KBPS, "125KBPS": CanSpeed.CAN_125KBPS, "250KBPS": CanSpeed.CAN_250KBPS, "500KBPS": CanSpeed.CAN_500KBPS, + "800KBPS": CanSpeed.CAN_800KBPS, "1000KBPS": CanSpeed.CAN_1000KBPS, } +CAN_SPEEDS_ESP32_S2 = { + "1KBPS": CanSpeed.CAN_1KBPS, + "5KBPS": CanSpeed.CAN_5KBPS, + "10KBPS": CanSpeed.CAN_10KBPS, + "12K5BPS": CanSpeed.CAN_12K5BPS, + "16KBPS": CanSpeed.CAN_16KBPS, + "20KBPS": CanSpeed.CAN_20KBPS, + **CAN_SPEEDS_ESP32, +} + +CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2} + +CAN_SPEEDS = { + VARIANT_ESP32: CAN_SPEEDS_ESP32, + VARIANT_ESP32S2: CAN_SPEEDS_ESP32_S2, + VARIANT_ESP32S3: CAN_SPEEDS_ESP32_S3, + VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3, + VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2, +} + + +def validate_bit_rate(value): + variant = get_esp32_variant() + if variant not in CAN_SPEEDS: + raise cv.Invalid(f"{variant} is not supported by component {esp32_can_ns}") + value = value.upper() + if value not in CAN_SPEEDS[variant]: + raise cv.Invalid(f"Bit rate {value} is not supported on {variant}") + return cv.enum(CAN_SPEEDS[variant])(value) + + 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.Optional(CONF_BIT_RATE, default="125KBPS"): validate_bit_rate, cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number, } diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index 3eb2d1f0354e..79e4b70f97f2 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -16,6 +16,30 @@ static const char *const TAG = "esp32_can"; static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) { switch (bitrate) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \ + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H6) + case canbus::CAN_1KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS(); + return true; + case canbus::CAN_5KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_5KBITS(); + return true; + case canbus::CAN_10KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_10KBITS(); + return true; + case canbus::CAN_12K5BPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_12_5KBITS(); + return true; + case canbus::CAN_16KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS(); + return true; + case canbus::CAN_20KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS(); + return true; +#endif + case canbus::CAN_25KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_25KBITS(); + return true; case canbus::CAN_50KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_50KBITS(); return true; @@ -31,6 +55,9 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config case canbus::CAN_500KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_500KBITS(); return true; + case canbus::CAN_800KBPS: + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_800KBITS(); + return true; case canbus::CAN_1000KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1MBITS(); return true; diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 7170a6dabf80..49d95d89e5c4 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -4,7 +4,7 @@ from esphome.const import CONF_ID -AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] +AUTO_LOAD = ["esp32_ble_server"] CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] @@ -22,20 +22,12 @@ ) -def validate_none_(value): - if value in ("none", "None"): - return None - if cv.boolean(value) is False: - return None - raise cv.Invalid("Must be none") - - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer), cv.Required(CONF_AUTHORIZER): cv.Any( - validate_none_, cv.use_id(binary_sensor.BinarySensor) + cv.none, cv.use_id(binary_sensor.BinarySensor) ), cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), cv.Optional( @@ -44,6 +36,9 @@ def validate_none_(value): cv.Optional( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WIFI_TIMEOUT, default="1min" + ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -61,6 +56,8 @@ async def to_code(config): cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) + cg.add(var.set_wifi_timeout(config[CONF_WIFI_TIMEOUT])) + if CONF_AUTHORIZER in config and config[CONF_AUTHORIZER] is not None: activator = await cg.get_variable(config[CONF_AUTHORIZER]) cg.add(var.set_authorizer(activator)) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 5ff4b0827d95..19340c3dd86c 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -18,6 +18,17 @@ ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } void ESP32ImprovComponent::setup() { this->service_ = global_ble_server->create_service(improv::SERVICE_UUID, true); this->setup_characteristics(); + +#ifdef USE_BINARY_SENSOR + if (this->authorizer_ != nullptr) { + this->authorizer_->add_on_state_callback([this](bool state) { + if (state) { + this->authorized_start_ = millis(); + this->identify_start_ = 0; + } + }); + } +#endif } void ESP32ImprovComponent::setup_characteristics() { @@ -50,8 +61,10 @@ void ESP32ImprovComponent::setup_characteristics() { BLEDescriptor *capabilities_descriptor = new BLE2902(); this->capabilities_->add_descriptor(capabilities_descriptor); uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT if (this->status_indicator_ != nullptr) capabilities |= improv::CAPABILITY_IDENTIFY; +#endif this->capabilities_->set_value(capabilities); this->setup_complete_ = true; } @@ -63,8 +76,7 @@ void ESP32ImprovComponent::loop() { switch (this->state_) { case improv::STATE_STOPPED: - if (this->status_indicator_ != nullptr) - this->status_indicator_->turn_off(); + this->set_status_indicator_state_(false); if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { if (this->service_->is_running()) { @@ -80,18 +92,22 @@ void ESP32ImprovComponent::loop() { } break; case improv::STATE_AWAITING_AUTHORIZATION: { - if (this->authorizer_ == nullptr || this->authorizer_->state) { +#ifdef USE_BINARY_SENSOR + if (this->authorizer_ == nullptr || + (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) { this->set_state_(improv::STATE_AUTHORIZED); - this->authorized_start_ = now; - } else { - if (this->status_indicator_ != nullptr) { - if (!this->check_identify_()) - this->status_indicator_->turn_on(); - } + } else +#else + this->set_state_(improv::STATE_AUTHORIZED); +#endif + { + if (!this->check_identify_()) + this->set_status_indicator_state_(true); } break; } case improv::STATE_AUTHORIZED: { +#ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) { if (now - this->authorized_start_ > this->authorized_duration_) { ESP_LOGD(TAG, "Authorization timeout"); @@ -99,25 +115,14 @@ void ESP32ImprovComponent::loop() { return; } } - if (this->status_indicator_ != nullptr) { - if (!this->check_identify_()) { - if ((now % 1000) < 500) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } - } +#endif + if (!this->check_identify_()) { + this->set_status_indicator_state_((now % 1000) < 500); } break; } case improv::STATE_PROVISIONING: { - if (this->status_indicator_ != nullptr) { - if ((now % 200) < 100) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } - } + this->set_status_indicator_state_((now % 200) < 100); if (wifi::global_wifi_component->is_connected()) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); @@ -142,13 +147,27 @@ void ESP32ImprovComponent::loop() { } case improv::STATE_PROVISIONED: { this->incoming_data_.clear(); - if (this->status_indicator_ != nullptr) - this->status_indicator_->turn_off(); + this->set_status_indicator_state_(false); break; } } } +void ESP32ImprovComponent::set_status_indicator_state_(bool state) { +#ifdef USE_OUTPUT + if (this->status_indicator_ == nullptr) + return; + if (this->status_indicator_state_ == state) + return; + this->status_indicator_state_ = state; + if (state) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } +#endif +} + bool ESP32ImprovComponent::check_identify_() { uint32_t now = millis(); @@ -156,11 +175,7 @@ bool ESP32ImprovComponent::check_identify_() { if (identify) { uint32_t time = now % 1000; - if (time < 600 && time % 200 < 100) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } + this->set_status_indicator_state_(time < 600 && time % 200 < 100); } return identify; } @@ -174,6 +189,25 @@ void ESP32ImprovComponent::set_state_(improv::State state) { if (state != improv::STATE_STOPPED) this->status_->notify(); } + std::vector service_data(8, 0); + service_data[0] = 0x77; // PR + service_data[1] = 0x46; // IM + service_data[2] = static_cast(state); + + uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; +#endif + + service_data[3] = capabilities; + service_data[4] = 0x00; // Reserved + service_data[5] = 0x00; // Reserved + service_data[6] = 0x00; // Reserved + service_data[7] = 0x00; // Reserved + + esp32_ble::global_ble->get_advertising()->set_service_data(service_data); + esp32_ble::global_ble->get_advertising()->start(); } void ESP32ImprovComponent::set_error_(improv::Error error) { @@ -195,7 +229,7 @@ void ESP32ImprovComponent::send_response_(std::vector &response) { } void ESP32ImprovComponent::start() { - if (this->state_ != improv::STATE_STOPPED) + if (this->should_start_ || this->state_ != improv::STATE_STOPPED) return; ESP_LOGD(TAG, "Setting Improv to start"); @@ -213,8 +247,12 @@ float ESP32ImprovComponent::get_setup_priority() const { return setup_priority:: void ESP32ImprovComponent::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 Improv:"); +#ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_); +#endif +#ifdef USE_OUTPUT ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr)); +#endif } void ESP32ImprovComponent::process_incoming_data_() { @@ -273,8 +311,10 @@ void ESP32ImprovComponent::process_incoming_data_() { void ESP32ImprovComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); +#ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) this->authorized_start_ = millis(); +#endif ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); wifi::global_wifi_component->clear_sta(); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 1a142c94b6d1..00c6cf885a7d 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -1,14 +1,22 @@ #pragma once -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/esp32_ble_server/ble_characteristic.h" -#include "esphome/components/esp32_ble_server/ble_server.h" -#include "esphome/components/output/binary_output.h" -#include "esphome/components/wifi/wifi_component.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/components/esp32_ble_server/ble_characteristic.h" +#include "esphome/components/esp32_ble_server/ble_server.h" +#include "esphome/components/wifi/wifi_component.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_OUTPUT +#include "esphome/components/output/binary_output.h" +#endif + #include #ifdef USE_ESP32 @@ -34,11 +42,18 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void stop() override; bool is_active() const { return this->state_ != improv::STATE_STOPPED; } +#ifdef USE_BINARY_SENSOR void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; } +#endif +#ifdef USE_OUTPUT void set_status_indicator(output::BinaryOutput *status_indicator) { this->status_indicator_ = status_indicator; } +#endif void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; } void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; } + void set_wifi_timeout(uint32_t wifi_timeout) { this->wifi_timeout_ = wifi_timeout; } + uint32_t get_wifi_timeout() const { return this->wifi_timeout_; } + protected: bool should_start_{false}; bool setup_complete_{false}; @@ -48,6 +63,8 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { uint32_t authorized_start_{0}; uint32_t authorized_duration_; + uint32_t wifi_timeout_{}; + std::vector incoming_data_; wifi::WiFiAP connecting_sta_; @@ -58,12 +75,19 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { BLECharacteristic *rpc_response_; BLECharacteristic *capabilities_; +#ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *authorizer_{nullptr}; +#endif +#ifdef USE_OUTPUT output::BinaryOutput *status_indicator_{nullptr}; +#endif improv::State state_{improv::STATE_STOPPED}; improv::Error error_state_{improv::ERROR_NONE}; + bool status_indicator_state_{false}; + void set_status_indicator_state_(bool state); + void set_state_(improv::State state); void set_error_(improv::Error error); void send_response_(std::vector &response); diff --git a/esphome/components/esp32_rmt_led_strip/__init__.py b/esphome/components/esp32_rmt_led_strip/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp new file mode 100644 index 000000000000..df6ee2ce2f2f --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -0,0 +1,208 @@ +#include +#include "led_strip.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace esp32_rmt_led_strip { + +static const char *const TAG = "esp32_rmt_led_strip"; + +static const uint8_t RMT_CLK_DIV = 2; + +void ESP32RMTLEDStripLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip..."); + + size_t buffer_size = this->get_buffer_size_(); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buf_ = allocator.allocate(buffer_size); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate LED buffer!"); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(this->num_leds_); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate effect data!"); + this->mark_failed(); + return; + } + + ExternalRAMAllocator rmt_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit + + rmt_config_t config; + memset(&config, 0, sizeof(config)); + config.channel = this->channel_; + config.rmt_mode = RMT_MODE_TX; + config.gpio_num = gpio_num_t(this->pin_); + config.mem_block_num = 1; + config.clk_div = RMT_CLK_DIV; + config.tx_config.loop_en = false; + config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; + config.tx_config.carrier_en = false; + config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; + config.tx_config.idle_output_en = true; + + if (rmt_config(&config) != ESP_OK) { + ESP_LOGE(TAG, "Cannot initialize RMT!"); + this->mark_failed(); + return; + } + if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { + ESP_LOGE(TAG, "Cannot install RMT driver!"); + this->mark_failed(); + return; + } +} + +void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, + uint32_t bit1_low) { + float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; + + // 0-bit + this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); + this->bit0_.level0 = 1; + this->bit0_.duration1 = (uint32_t) (ratio * bit0_low); + this->bit0_.level1 = 0; + // 1-bit + this->bit1_.duration0 = (uint32_t) (ratio * bit1_high); + this->bit1_.level0 = 1; + this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); + this->bit1_.level1 = 0; +} + +void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { + // protect from refreshing too often + uint32_t now = micros(); + if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + // try again next loop iteration, so that this change won't get lost + this->schedule_show(); + return; + } + this->last_refresh_ = now; + this->mark_shown_(); + + ESP_LOGVV(TAG, "Writing RGB values to bus..."); + + if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) { + ESP_LOGE(TAG, "RMT TX timeout"); + this->status_set_warning(); + return; + } + delayMicroseconds(50); + + size_t buffer_size = this->get_buffer_size_(); + + size_t size = 0; + size_t len = 0; + uint8_t *psrc = this->buf_; + rmt_item32_t *pdest = this->rmt_buf_; + while (size < buffer_size) { + uint8_t b = *psrc; + for (int i = 0; i < 8; i++) { + pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val; + pdest++; + len++; + } + size++; + psrc++; + } + + if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { + ESP_LOGE(TAG, "RMT TX error"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); +} + +light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const { + int32_t r = 0, g = 0, b = 0; + switch (this->rgb_order_) { + case ORDER_RGB: + r = 0; + g = 1; + b = 2; + break; + case ORDER_RBG: + r = 0; + g = 2; + b = 1; + break; + case ORDER_GRB: + r = 1; + g = 0; + b = 2; + break; + case ORDER_GBR: + r = 2; + g = 0; + b = 1; + break; + case ORDER_BGR: + r = 2; + g = 1; + b = 0; + break; + case ORDER_BRG: + r = 1; + g = 2; + b = 0; + break; + } + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + return {this->buf_ + (index * multiplier) + r, + this->buf_ + (index * multiplier) + g, + this->buf_ + (index * multiplier) + b, + this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + &this->effect_data_[index], + &this->correction_}; +} + +void ESP32RMTLEDStripLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); + const char *rgb_order; + switch (this->rgb_order_) { + case ORDER_RGB: + rgb_order = "RGB"; + break; + case ORDER_RBG: + rgb_order = "RBG"; + break; + case ORDER_GRB: + rgb_order = "GRB"; + break; + case ORDER_GBR: + rgb_order = "GBR"; + break; + case ORDER_BGR: + rgb_order = "BGR"; + break; + case ORDER_BRG: + rgb_order = "BRG"; + break; + default: + rgb_order = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); + ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); +} + +float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h new file mode 100644 index 000000000000..11d61b07e157 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -0,0 +1,87 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/light_output.h" +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include +#include + +namespace esphome { +namespace esp32_rmt_led_strip { + +enum RGBOrder : uint8_t { + ORDER_RGB, + ORDER_RBG, + ORDER_GRB, + ORDER_GBR, + ORDER_BGR, + ORDER_BRG, +}; + +class ESP32RMTLEDStripLightOutput : public light::AddressableLight { + public: + void setup() override; + void write_state(light::LightState *state) override; + float get_setup_priority() const override; + + int32_t size() const override { return this->num_leds_; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + if (this->is_rgbw_) { + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); + } else { + traits.set_supported_color_modes({light::ColorMode::RGB}); + } + return traits; + } + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } + void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } + + /// Set a maximum refresh rate in µs as some lights do not like being updated too often. + void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } + + void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); + + void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } + void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + + void dump_config() override; + + protected: + light::ESPColorView get_view_internal(int32_t index) const override; + + size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + + uint8_t *buf_{nullptr}; + uint8_t *effect_data_{nullptr}; + rmt_item32_t *rmt_buf_{nullptr}; + + uint8_t pin_; + uint16_t num_leds_; + bool is_rgbw_; + + rmt_item32_t bit0_, bit1_; + RGBOrder rgb_order_; + rmt_channel_t channel_; + + uint32_t last_refresh_{0}; + optional max_refresh_rate_{}; +}; + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py new file mode 100644 index 000000000000..43629bec51ef --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -0,0 +1,153 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import esp32, light +from esphome.const import ( + CONF_CHIPSET, + CONF_MAX_REFRESH_RATE, + CONF_NUM_LEDS, + CONF_OUTPUT_ID, + CONF_PIN, + CONF_RGB_ORDER, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] + +esp32_rmt_led_strip_ns = cg.esphome_ns.namespace("esp32_rmt_led_strip") +ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_( + "ESP32RMTLEDStripLightOutput", light.AddressableLight +) + +rmt_channel_t = cg.global_ns.enum("rmt_channel_t") + +RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder") + +RGB_ORDERS = { + "RGB": RGBOrder.ORDER_RGB, + "RBG": RGBOrder.ORDER_RBG, + "GRB": RGBOrder.ORDER_GRB, + "GBR": RGBOrder.ORDER_GBR, + "BGR": RGBOrder.ORDER_BGR, + "BRG": RGBOrder.ORDER_BRG, +} + + +@dataclass +class LEDStripTimings: + bit0_high: int + bit0_low: int + bit1_high: int + bit1_low: int + + +CHIPSETS = { + "WS2812": LEDStripTimings(400, 1000, 1000, 400), + "SK6812": LEDStripTimings(300, 900, 600, 600), + "APA106": LEDStripTimings(350, 1360, 1360, 350), + "SM16703": LEDStripTimings(300, 900, 1360, 350), +} + + +CONF_IS_RGBW = "is_rgbw" +CONF_BIT0_HIGH = "bit0_high" +CONF_BIT0_LOW = "bit0_low" +CONF_BIT1_HIGH = "bit1_high" +CONF_BIT1_LOW = "bit1_low" +CONF_RMT_CHANNEL = "rmt_channel" + +RMT_CHANNELS = { + esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], + esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32C3: [0, 1], + esp32.const.VARIANT_ESP32C6: [0, 1], + esp32.const.VARIANT_ESP32H2: [0, 1], +} + + +def _validate_rmt_channel(value): + variant = esp32.get_esp32_variant() + if variant not in RMT_CHANNELS: + raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") + if value not in RMT_CHANNELS[variant]: + raise cv.Invalid( + f"RMT channel {value} is not supported for ESP32 variant {variant}." + ) + return value + + +CONFIG_SCHEMA = cv.All( + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, + cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Required(CONF_RMT_CHANNEL): _validate_rmt_channel, + cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, + cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Inclusive( + CONF_BIT0_HIGH, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT0_LOW, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT1_HIGH, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT1_LOW, + "custom", + ): cv.positive_time_period_microseconds, + } + ), + cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + await cg.register_component(var, config) + + cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + cg.add(var.set_pin(config[CONF_PIN])) + + if CONF_MAX_REFRESH_RATE in config: + cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) + + if CONF_CHIPSET in config: + chipset = CHIPSETS[config[CONF_CHIPSET]] + cg.add( + var.set_led_params( + chipset.bit0_high, + chipset.bit0_low, + chipset.bit1_high, + chipset.bit1_low, + ) + ) + else: + cg.add( + var.set_led_params( + config[CONF_BIT0_HIGH], + config[CONF_BIT0_LOW], + config[CONF_BIT1_HIGH], + config[CONF_BIT1_LOW], + ) + ) + + cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) + cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + + cg.add( + var.set_rmt_channel( + getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") + ) + ) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index cdf6aa3abde1..0180d1810417 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -11,25 +11,113 @@ CONF_VOLTAGE_ATTENUATION, ) from esphome.core import TimePeriod +from esphome.components import esp32 +from esphome.components.esp32 import get_esp32_variant, gpio +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] +CONF_DEBOUNCE_COUNT = "debounce_count" +CONF_DENOISE_GRADE = "denoise_grade" +CONF_DENOISE_CAP_LEVEL = "denoise_cap_level" +CONF_FILTER_MODE = "filter_mode" +CONF_NOISE_THRESHOLD = "noise_threshold" +CONF_JITTER_STEP = "jitter_step" +CONF_SMOOTH_MODE = "smooth_mode" +CONF_WATERPROOF_GUARD_RING = "waterproof_guard_ring" +CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver" + esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) +TOUCH_PADS = { + VARIANT_ESP32: { + 4: cg.global_ns.TOUCH_PAD_NUM0, + 0: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 15: cg.global_ns.TOUCH_PAD_NUM3, + 13: cg.global_ns.TOUCH_PAD_NUM4, + 12: cg.global_ns.TOUCH_PAD_NUM5, + 14: cg.global_ns.TOUCH_PAD_NUM6, + 27: cg.global_ns.TOUCH_PAD_NUM7, + 33: cg.global_ns.TOUCH_PAD_NUM8, + 32: cg.global_ns.TOUCH_PAD_NUM9, + }, + VARIANT_ESP32S2: { + 1: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 3: cg.global_ns.TOUCH_PAD_NUM3, + 4: cg.global_ns.TOUCH_PAD_NUM4, + 5: cg.global_ns.TOUCH_PAD_NUM5, + 6: cg.global_ns.TOUCH_PAD_NUM6, + 7: cg.global_ns.TOUCH_PAD_NUM7, + 8: cg.global_ns.TOUCH_PAD_NUM8, + 9: cg.global_ns.TOUCH_PAD_NUM9, + 10: cg.global_ns.TOUCH_PAD_NUM10, + 11: cg.global_ns.TOUCH_PAD_NUM11, + 12: cg.global_ns.TOUCH_PAD_NUM12, + 13: cg.global_ns.TOUCH_PAD_NUM13, + 14: cg.global_ns.TOUCH_PAD_NUM14, + }, + VARIANT_ESP32S3: { + 1: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 3: cg.global_ns.TOUCH_PAD_NUM3, + 4: cg.global_ns.TOUCH_PAD_NUM4, + 5: cg.global_ns.TOUCH_PAD_NUM5, + 6: cg.global_ns.TOUCH_PAD_NUM6, + 7: cg.global_ns.TOUCH_PAD_NUM7, + 8: cg.global_ns.TOUCH_PAD_NUM8, + 9: cg.global_ns.TOUCH_PAD_NUM9, + 10: cg.global_ns.TOUCH_PAD_NUM10, + 11: cg.global_ns.TOUCH_PAD_NUM11, + 12: cg.global_ns.TOUCH_PAD_NUM12, + 13: cg.global_ns.TOUCH_PAD_NUM13, + 14: cg.global_ns.TOUCH_PAD_NUM14, + }, +} -def validate_voltage(values): - def validator(value): - if isinstance(value, float) and value.is_integer(): - value = int(value) - value = cv.string(value) - if not value.endswith("V"): - value += "V" - return cv.one_of(*values)(value) - return validator +TOUCH_PAD_DENOISE_GRADE = { + "BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12, + "BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10, + "BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8, + "BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4, +} + +TOUCH_PAD_DENOISE_CAP_LEVEL = { + "L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0, + "L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1, + "L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2, + "L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3, + "L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4, + "L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5, + "L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6, + "L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7, +} + +TOUCH_PAD_FILTER_MODE = { + "IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4, + "IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8, + "IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16, + "IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32, + "IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64, + "IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128, + "IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256, + "JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER, +} +TOUCH_PAD_SMOOTH_MODE = { + "OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF, + "IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2, + "IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4, + "IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8, +} LOW_VOLTAGE_REFERENCE = { "0.5V": cg.global_ns.TOUCH_LVOLT_0V5, @@ -49,31 +137,131 @@ def validator(value): "0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, } +TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = { + "L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0, + "L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1, + "L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2, + "L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3, + "L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4, + "L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5, + "L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6, + "L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7, +} + + +def validate_touch_pad(value): + value = gpio.validate_gpio_pin(value) + variant = get_esp32_variant() + if variant not in TOUCH_PADS: + raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ESP32TouchComponent), - cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, - cv.Optional( - CONF_IIR_FILTER, default="0ms" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SLEEP_DURATION, default="27306us"): cv.All( - cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906)) - ), - cv.Optional(CONF_MEASUREMENT_DURATION, default="8192us"): cv.All( - cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=8192)) - ), - cv.Optional(CONF_LOW_VOLTAGE_REFERENCE, default="0.5V"): validate_voltage( - LOW_VOLTAGE_REFERENCE - ), - cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( - HIGH_VOLTAGE_REFERENCE - ), - cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( - VOLTAGE_ATTENUATION - ), - } -).extend(cv.COMPONENT_SCHEMA) + pads = TOUCH_PADS[variant] + if value not in pads: + raise cv.Invalid(f"Pin {value} does not support touch pads.") + return cv.enum(pads)(value) + + +def validate_variant_vars(config): + if get_esp32_variant() == VARIANT_ESP32: + variant_vars = { + CONF_DEBOUNCE_COUNT, + CONF_DENOISE_GRADE, + CONF_DENOISE_CAP_LEVEL, + CONF_FILTER_MODE, + CONF_NOISE_THRESHOLD, + CONF_JITTER_STEP, + CONF_SMOOTH_MODE, + CONF_WATERPROOF_GUARD_RING, + CONF_WATERPROOF_SHIELD_DRIVER, + } + for vvar in variant_vars: + if vvar in config: + raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}") + elif ( + get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3 + ) and CONF_IIR_FILTER in config: + raise cv.Invalid( + f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}" + ) + + return config + + +def validate_voltage(values): + def validator(value): + if isinstance(value, float) and value.is_integer(): + value = int(value) + value = cv.string(value) + if not value.endswith("V"): + value += "V" + return cv.one_of(*values)(value) + + return validator + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32TouchComponent), + cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, + # common options + cv.Optional(CONF_SLEEP_DURATION, default="27306us"): cv.All( + cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906)) + ), + cv.Optional(CONF_MEASUREMENT_DURATION, default="8192us"): cv.All( + cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=8192)) + ), + cv.Optional(CONF_LOW_VOLTAGE_REFERENCE, default="0.5V"): validate_voltage( + LOW_VOLTAGE_REFERENCE + ), + cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( + HIGH_VOLTAGE_REFERENCE + ), + cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( + VOLTAGE_ATTENUATION + ), + # ESP32 only + cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds, + # ESP32-S2/S3 only + cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7), + cv.Optional(CONF_FILTER_MODE): cv.enum( + TOUCH_PAD_FILTER_MODE, upper=True, space="_" + ), + cv.Optional(CONF_NOISE_THRESHOLD): cv.int_range(min=0, max=3), + cv.Optional(CONF_JITTER_STEP): cv.int_range(min=0, max=15), + cv.Optional(CONF_SMOOTH_MODE): cv.enum( + TOUCH_PAD_SMOOTH_MODE, upper=True, space="_" + ), + cv.Optional(CONF_DENOISE_GRADE): cv.enum( + TOUCH_PAD_DENOISE_GRADE, upper=True, space="_" + ), + cv.Optional(CONF_DENOISE_CAP_LEVEL): cv.enum( + TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_" + ), + cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad, + cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum( + TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_" + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL), + cv.has_none_or_all_keys( + CONF_DEBOUNCE_COUNT, + CONF_FILTER_MODE, + CONF_NOISE_THRESHOLD, + CONF_JITTER_STEP, + CONF_SMOOTH_MODE, + ), + cv.has_none_or_all_keys(CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32, + esp32.const.VARIANT_ESP32S2, + esp32.const.VARIANT_ESP32S3, + ] + ), + validate_variant_vars, +) async def to_code(config): @@ -81,7 +269,6 @@ async def to_code(config): await cg.register_component(touch, config) cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE])) - cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15)) cg.add(touch.set_sleep_duration(sleep_duration)) @@ -106,3 +293,33 @@ async def to_code(config): VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]] ) ) + + if get_esp32_variant() == VARIANT_ESP32: + if CONF_IIR_FILTER in config: + cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) + + if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: + if CONF_FILTER_MODE in config: + cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE])) + if CONF_DEBOUNCE_COUNT in config: + cg.add(touch.set_debounce_count(config[CONF_DEBOUNCE_COUNT])) + if CONF_NOISE_THRESHOLD in config: + cg.add(touch.set_noise_threshold(config[CONF_NOISE_THRESHOLD])) + if CONF_JITTER_STEP in config: + cg.add(touch.set_jitter_step(config[CONF_JITTER_STEP])) + if CONF_SMOOTH_MODE in config: + cg.add(touch.set_smooth_level(config[CONF_SMOOTH_MODE])) + if CONF_DENOISE_GRADE in config: + cg.add(touch.set_denoise_grade(config[CONF_DENOISE_GRADE])) + if CONF_DENOISE_CAP_LEVEL in config: + cg.add(touch.set_denoise_cap(config[CONF_DENOISE_CAP_LEVEL])) + if CONF_WATERPROOF_GUARD_RING in config: + cg.add( + touch.set_waterproof_guard_ring_pad(config[CONF_WATERPROOF_GUARD_RING]) + ) + if CONF_WATERPROOF_SHIELD_DRIVER in config: + cg.add( + touch.set_waterproof_shield_driver( + config[CONF_WATERPROOF_SHIELD_DRIVER] + ) + ) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index 326f55983024..e9322b3080b6 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -6,35 +6,13 @@ CONF_THRESHOLD, CONF_ID, ) -from esphome.components.esp32 import gpio -from . import esp32_touch_ns, ESP32TouchComponent +from . import esp32_touch_ns, ESP32TouchComponent, validate_touch_pad DEPENDENCIES = ["esp32_touch", "esp32"] CONF_ESP32_TOUCH_ID = "esp32_touch_id" CONF_WAKEUP_THRESHOLD = "wakeup_threshold" -TOUCH_PADS = { - 4: cg.global_ns.TOUCH_PAD_NUM0, - 0: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 15: cg.global_ns.TOUCH_PAD_NUM3, - 13: cg.global_ns.TOUCH_PAD_NUM4, - 12: cg.global_ns.TOUCH_PAD_NUM5, - 14: cg.global_ns.TOUCH_PAD_NUM6, - 27: cg.global_ns.TOUCH_PAD_NUM7, - 33: cg.global_ns.TOUCH_PAD_NUM8, - 32: cg.global_ns.TOUCH_PAD_NUM9, -} - - -def validate_touch_pad(value): - value = gpio.validate_gpio_pin(value) - if value not in TOUCH_PADS: - raise cv.Invalid(f"Pin {value} does not support touch pads.") - return value - - ESP32TouchBinarySensor = esp32_touch_ns.class_( "ESP32TouchBinarySensor", binary_sensor.BinarySensor ) @@ -43,8 +21,8 @@ def validate_touch_pad(value): { cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_id(ESP32TouchComponent), cv.Required(CONF_PIN): validate_touch_pad, - cv.Required(CONF_THRESHOLD): cv.uint16_t, - cv.Optional(CONF_WAKEUP_THRESHOLD, default=0): cv.uint16_t, + cv.Required(CONF_THRESHOLD): cv.uint32_t, + cv.Optional(CONF_WAKEUP_THRESHOLD, default=0): cv.uint32_t, } ) @@ -53,7 +31,7 @@ async def to_code(config): hub = await cg.get_variable(config[CONF_ESP32_TOUCH_ID]) var = cg.new_Pvariable( config[CONF_ID], - TOUCH_PADS[config[CONF_PIN]], + config[CONF_PIN], config[CONF_THRESHOLD], config[CONF_WAKEUP_THRESHOLD], ) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 0e3d3d9fd543..e43c3b844c8e 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -5,6 +5,8 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace esp32_touch { @@ -13,18 +15,58 @@ static const char *const TAG = "esp32_touch"; void ESP32TouchComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 Touch Hub..."); touch_pad_init(); +// set up and enable/start filtering based on ESP32 variant +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + if (this->filter_configured_()) { + touch_filter_config_t filter_info = { + .mode = this->filter_mode_, + .debounce_cnt = this->debounce_count_, + .noise_thr = this->noise_threshold_, + .jitter_step = this->jitter_step_, + .smh_lvl = this->smooth_level_, + }; + touch_pad_filter_set_config(&filter_info); + touch_pad_filter_enable(); + } + + if (this->denoise_configured_()) { + touch_pad_denoise_t denoise = { + .grade = this->grade_, + .cap_level = this->cap_level_, + }; + touch_pad_denoise_set_config(&denoise); + touch_pad_denoise_enable(); + } + if (this->waterproof_configured_()) { + touch_pad_waterproof_t waterproof = { + .guard_ring_pad = this->waterproof_guard_ring_pad_, + .shield_driver = this->waterproof_shield_driver_, + }; + touch_pad_waterproof_set_config(&waterproof); + touch_pad_waterproof_enable(); + } +#else if (this->iir_filter_enabled_()) { touch_pad_filter_start(this->iir_filter_); } +#endif touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); for (auto *child : this->children_) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + touch_pad_config(child->get_touch_pad()); +#else // Disable interrupt threshold touch_pad_config(child->get_touch_pad(), 0); +#endif } +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + touch_pad_fsm_start(); +#endif } void ESP32TouchComponent::dump_config() { @@ -92,38 +134,168 @@ void ESP32TouchComponent::dump_config() { } ESP_LOGCONFIG(TAG, " Voltage Attenuation: %s", atten_s); +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + if (this->filter_configured_()) { + const char *filter_mode_s; + switch (this->filter_mode_) { + case TOUCH_PAD_FILTER_IIR_4: + filter_mode_s = "IIR_4"; + break; + case TOUCH_PAD_FILTER_IIR_8: + filter_mode_s = "IIR_8"; + break; + case TOUCH_PAD_FILTER_IIR_16: + filter_mode_s = "IIR_16"; + break; + case TOUCH_PAD_FILTER_IIR_32: + filter_mode_s = "IIR_32"; + break; + case TOUCH_PAD_FILTER_IIR_64: + filter_mode_s = "IIR_64"; + break; + case TOUCH_PAD_FILTER_IIR_128: + filter_mode_s = "IIR_128"; + break; + case TOUCH_PAD_FILTER_IIR_256: + filter_mode_s = "IIR_256"; + break; + case TOUCH_PAD_FILTER_JITTER: + filter_mode_s = "JITTER"; + break; + default: + filter_mode_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Filter mode: %s", filter_mode_s); + ESP_LOGCONFIG(TAG, " Debounce count: %" PRIu32, this->debounce_count_); + ESP_LOGCONFIG(TAG, " Noise threshold coefficient: %" PRIu32, this->noise_threshold_); + ESP_LOGCONFIG(TAG, " Jitter filter step size: %" PRIu32, this->jitter_step_); + const char *smooth_level_s; + switch (this->smooth_level_) { + case TOUCH_PAD_SMOOTH_OFF: + smooth_level_s = "OFF"; + break; + case TOUCH_PAD_SMOOTH_IIR_2: + smooth_level_s = "IIR_2"; + break; + case TOUCH_PAD_SMOOTH_IIR_4: + smooth_level_s = "IIR_4"; + break; + case TOUCH_PAD_SMOOTH_IIR_8: + smooth_level_s = "IIR_8"; + break; + default: + smooth_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); + } + + if (this->denoise_configured_()) { + const char *grade_s; + switch (this->grade_) { + case TOUCH_PAD_DENOISE_BIT12: + grade_s = "BIT12"; + break; + case TOUCH_PAD_DENOISE_BIT10: + grade_s = "BIT10"; + break; + case TOUCH_PAD_DENOISE_BIT8: + grade_s = "BIT8"; + break; + case TOUCH_PAD_DENOISE_BIT4: + grade_s = "BIT4"; + break; + default: + grade_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); + + const char *cap_level_s; + switch (this->cap_level_) { + case TOUCH_PAD_DENOISE_CAP_L0: + cap_level_s = "L0"; + break; + case TOUCH_PAD_DENOISE_CAP_L1: + cap_level_s = "L1"; + break; + case TOUCH_PAD_DENOISE_CAP_L2: + cap_level_s = "L2"; + break; + case TOUCH_PAD_DENOISE_CAP_L3: + cap_level_s = "L3"; + break; + case TOUCH_PAD_DENOISE_CAP_L4: + cap_level_s = "L4"; + break; + case TOUCH_PAD_DENOISE_CAP_L5: + cap_level_s = "L5"; + break; + case TOUCH_PAD_DENOISE_CAP_L6: + cap_level_s = "L6"; + break; + case TOUCH_PAD_DENOISE_CAP_L7: + cap_level_s = "L7"; + break; + default: + cap_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); + } +#else if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %ums", this->iir_filter_); + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); } else { ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); } +#endif + if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED!"); + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); } for (auto *child : this->children_) { LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, " Pad: T%d", child->get_touch_pad()); - ESP_LOGCONFIG(TAG, " Threshold: %u", child->get_threshold()); + ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); + ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); } } +uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + uint32_t value = 0; + if (this->filter_configured_()) { + touch_pad_filter_read_smooth(tp, &value); + } else { + touch_pad_read_raw_data(tp, &value); + } +#else + uint16_t value = 0; + if (this->iir_filter_enabled_()) { + touch_pad_read_filtered(tp, &value); + } else { + touch_pad_read(tp, &value); + } +#endif + return value; +} + void ESP32TouchComponent::loop() { const uint32_t now = millis(); bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { - uint16_t value; - if (this->iir_filter_enabled_()) { - touch_pad_read_filtered(child->get_touch_pad(), &value); - } else { - touch_pad_read(child->get_touch_pad(), &value); - } - - child->value_ = value; - child->publish_state(value < child->get_threshold()); + child->value_ = this->component_touch_pad_read(child->get_touch_pad()); +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) + child->publish_state(child->value_ < child->get_threshold()); +#else + child->publish_state(child->value_ > child->get_threshold()); +#endif if (should_print) { - ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); + ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), + (uint32_t) child->get_touch_pad(), child->value_); } App.feed_wdt(); @@ -138,10 +310,12 @@ void ESP32TouchComponent::loop() { void ESP32TouchComponent::on_shutdown() { bool is_wakeup_source = false; +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) if (this->iir_filter_enabled_()) { touch_pad_filter_stop(); touch_pad_filter_delete(); } +#endif for (auto *child : this->children_) { if (child->get_wakeup_threshold() != 0) { @@ -151,8 +325,10 @@ void ESP32TouchComponent::on_shutdown() { touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); } +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) // No filter available when using as wake-up source. touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); +#endif } } @@ -161,7 +337,7 @@ void ESP32TouchComponent::on_shutdown() { } } -ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint16_t threshold, uint16_t wakeup_threshold) +ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} } // namespace esp32_touch diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index c954a14654f4..0eac590ce773 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -8,11 +8,7 @@ #include -#if ESP_IDF_VERSION_MAJOR >= 4 #include -#else -#include -#endif namespace esphome { namespace esp32_touch { @@ -21,25 +17,37 @@ class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { public: - void register_touch_pad(ESP32TouchBinarySensor *pad) { children_.push_back(pad); } - - void set_setup_mode(bool setup_mode) { setup_mode_ = setup_mode; } - - void set_iir_filter(uint32_t iir_filter) { iir_filter_ = iir_filter; } - - void set_sleep_duration(uint16_t sleep_duration) { sleep_cycle_ = sleep_duration; } - - void set_measurement_duration(uint16_t meas_cycle) { meas_cycle_ = meas_cycle; } + void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } + void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } + void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } + void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { - low_voltage_reference_ = low_voltage_reference; + this->low_voltage_reference_ = low_voltage_reference; } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { - high_voltage_reference_ = high_voltage_reference; + this->high_voltage_reference_ = high_voltage_reference; + } + void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { + this->voltage_attenuation_ = voltage_attenuation; } +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } + void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } + void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; } + void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; } + void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; } + void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; } + void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; } + void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; } + void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) { + this->waterproof_shield_driver_ = drive_capability; + } +#else + void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } +#endif - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { voltage_attenuation_ = voltage_attenuation; } + uint32_t component_touch_pad_read(touch_pad_t tp); void setup() override; void dump_config() override; @@ -49,38 +57,63 @@ class ESP32TouchComponent : public Component { void on_shutdown() override; protected: - /// Is the IIR filter enabled? - bool iir_filter_enabled_() const { return iir_filter_ > 0; } +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + bool filter_configured_() const { + return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); + } + bool denoise_configured_() const { + return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX); + } + bool waterproof_configured_() const { + return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && + (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); + } +#else + bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } +#endif - uint16_t sleep_cycle_{}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{}; - touch_high_volt_t high_voltage_reference_{}; - touch_volt_atten_t voltage_attenuation_{}; std::vector children_; bool setup_mode_{false}; - uint32_t setup_mode_last_log_print_{}; + uint32_t setup_mode_last_log_print_{0}; + // common parameters + uint16_t sleep_cycle_{4095}; + uint16_t meas_cycle_{65535}; + touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; + touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; + touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + uint32_t debounce_count_{0}; + uint32_t noise_threshold_{0}; + uint32_t jitter_step_{0}; + touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; + touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; + touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; + touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; +#else uint32_t iir_filter_{0}; +#endif }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint16_t threshold, uint16_t wakeup_threshold); + ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold); - touch_pad_t get_touch_pad() const { return touch_pad_; } - uint16_t get_threshold() const { return threshold_; } - void set_threshold(uint16_t threshold) { threshold_ = threshold; } - uint16_t get_value() const { return value_; } - uint16_t get_wakeup_threshold() const { return wakeup_threshold_; } + touch_pad_t get_touch_pad() const { return this->touch_pad_; } + uint32_t get_threshold() const { return this->threshold_; } + void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } + uint32_t get_value() const { return this->value_; } + uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: friend ESP32TouchComponent; - touch_pad_t touch_pad_; - uint16_t threshold_; - uint16_t value_; - const uint16_t wakeup_threshold_; + touch_pad_t touch_pad_{TOUCH_PAD_MAX}; + uint32_t threshold_{0}; + uint32_t value_{0}; + const uint32_t wakeup_threshold_{0}; }; } // namespace esp32_touch diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 8715a3b4e6c1..5336842b9d64 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -11,6 +11,7 @@ KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_ESP8266, ) from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv @@ -38,7 +39,7 @@ def set_core_data(config): CORE.data[KEY_ESP8266] = {} - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "esp8266" + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] @@ -50,6 +51,17 @@ def set_core_data(config): return config +def get_download_types(storage_json): + return [ + { + "title": "Standard format", + "description": "For flashing ESP8266.", + "file": "firmware.bin", + "download": f"{storage_json.name}.bin", + }, + ] + + def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/esp8266/Arduino/releases) version to # a PIO platformio/framework-arduinoespressif8266 value @@ -125,7 +137,7 @@ def _parse_platform_version(value): try: # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) - return f"platformio/espressif8266 @ {value}" + return f"platformio/espressif8266@{value}" except cv.Invalid: return value @@ -181,7 +193,7 @@ async def to_code(config): cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( "platform_packages", - [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], + [f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"], ) # Default for platformio is LWIP2_LOW_MEMORY with: @@ -240,7 +252,6 @@ async def to_code(config): # Called by writer.py def copy_files(): - dir = os.path.dirname(__file__) post_build_file = os.path.join(dir, "post_build.py.script") copy_file_if_changed( diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index d4b207852405..e75578cc1652 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from esphome.const import ( + CONF_ANALOG, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -140,7 +141,6 @@ def validate_supports(value): return value -CONF_ANALOG = "analog" ESP8266_PIN_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index b3614d8fcfa0..6f0f3741dd52 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -33,14 +33,30 @@ "RTL8201": EthernetType.ETHERNET_TYPE_RTL8201, "DP83848": EthernetType.ETHERNET_TYPE_DP83848, "IP101": EthernetType.ETHERNET_TYPE_IP101, + "JL1101": EthernetType.ETHERNET_TYPE_JL1101, + "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, + "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, } +emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") CLK_MODES = { - "GPIO0_IN": emac_rmii_clock_gpio_t.EMAC_CLK_IN_GPIO, - "GPIO0_OUT": emac_rmii_clock_gpio_t.EMAC_APPL_CLK_OUT_GPIO, - "GPIO16_OUT": emac_rmii_clock_gpio_t.EMAC_CLK_OUT_GPIO, - "GPIO17_OUT": emac_rmii_clock_gpio_t.EMAC_CLK_OUT_180_GPIO, + "GPIO0_IN": ( + emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, + emac_rmii_clock_gpio_t.EMAC_CLK_IN_GPIO, + ), + "GPIO0_OUT": ( + emac_rmii_clock_mode_t.EMAC_CLK_OUT, + emac_rmii_clock_gpio_t.EMAC_APPL_CLK_OUT_GPIO, + ), + "GPIO16_OUT": ( + emac_rmii_clock_mode_t.EMAC_CLK_OUT, + emac_rmii_clock_gpio_t.EMAC_CLK_OUT_GPIO, + ), + "GPIO17_OUT": ( + emac_rmii_clock_mode_t.EMAC_CLK_OUT, + emac_rmii_clock_gpio_t.EMAC_CLK_OUT_180_GPIO, + ), } @@ -113,7 +129,7 @@ async def to_code(config): cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) cg.add(var.set_type(config[CONF_TYPE])) - cg.add(var.set_clk_mode(CLK_MODES[config[CONF_CLK_MODE]])) + cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_POWER_PIN in config: diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c new file mode 100644 index 000000000000..de2a6f4f3551 --- /dev/null +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -0,0 +1,355 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef USE_ESP32 + +#include +#include +#include +#include "esp_log.h" +#include "esp_eth.h" +#if ESP_IDF_VERSION_MAJOR >= 5 +#include "esp_eth_phy_802_3.h" +#else +#include "eth_phy_regs_struct.h" +#endif +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_rom_gpio.h" +#include "esp_rom_sys.h" + +static const char *TAG = "jl1101"; +#define PHY_CHECK(a, str, goto_tag, ...) \ + do { \ + if (!(a)) { \ + ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + goto goto_tag; \ + } \ + } while (0) + +/***************Vendor Specific Register***************/ + +/** + * @brief PSR(Page Select Register) + * + */ +typedef union { + struct { + uint16_t page_select : 8; /* Select register page, default is 0 */ + uint16_t reserved : 8; /* Reserved */ + }; + uint16_t val; +} psr_reg_t; +#define ETH_PHY_PSR_REG_ADDR (0x1F) + +typedef struct { + esp_eth_phy_t parent; + esp_eth_mediator_t *eth; + int addr; + uint32_t reset_timeout_ms; + uint32_t autonego_timeout_ms; + eth_link_t link_status; + int reset_gpio_num; +} phy_jl1101_t; + +static esp_err_t jl1101_page_select(phy_jl1101_t *jl1101, uint32_t page) { + esp_eth_mediator_t *eth = jl1101->eth; + psr_reg_t psr = {.page_select = page}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_PSR_REG_ADDR, psr.val) == ESP_OK, "write PSR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_update_link_duplex_speed(phy_jl1101_t *jl1101) { + esp_eth_mediator_t *eth = jl1101->eth; + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + bmcr_reg_t bmcr; + bmsr_reg_t bmsr; + uint32_t peer_pause_ability = false; + anlpar_reg_t anlpar; + PHY_CHECK(jl1101_page_select(jl1101, 0) == ESP_OK, "select page 0 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)) == ESP_OK, + "read ANLPAR failed", err); + eth_link_t link = bmsr.link_status ? ETH_LINK_UP : ETH_LINK_DOWN; + /* check if link status changed */ + if (jl1101->link_status != link) { + /* when link up, read negotiation result */ + if (link == ETH_LINK_UP) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.speed_select) { + speed = ETH_SPEED_100M; + } else { + speed = ETH_SPEED_10M; + } + if (bmcr.duplex_mode) { + duplex = ETH_DUPLEX_FULL; + } else { + duplex = ETH_DUPLEX_HALF; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *) speed) == ESP_OK, "change speed failed", err); + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *) duplex) == ESP_OK, "change duplex failed", err); + /* if we're in duplex mode, and peer has the flow control ability */ + if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { + peer_pause_ability = 1; + } else { + peer_pause_ability = 0; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *) peer_pause_ability) == ESP_OK, + "change pause ability failed", err); + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_LINK, (void *) link) == ESP_OK, "change link failed", err); + jl1101->link_status = link; + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_mediator(esp_eth_phy_t *phy, esp_eth_mediator_t *eth) { + PHY_CHECK(eth, "can't set mediator to null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->eth = eth; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_get_link(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + /* Updata information about link, speed, duplex */ + PHY_CHECK(jl1101_update_link_duplex_speed(jl1101) == ESP_OK, "update link duplex speed failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->link_status = ETH_LINK_DOWN; + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr = {.reset = 1}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for reset complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 50; to++) { + vTaskDelay(pdMS_TO_TICKS(50)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!bmcr.reset) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 50, "reset timeout", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + if (jl1101->reset_gpio_num >= 0) { + esp_rom_gpio_pad_select_gpio(jl1101->reset_gpio_num); + gpio_set_direction(jl1101->reset_gpio_num, GPIO_MODE_OUTPUT); + gpio_set_level(jl1101->reset_gpio_num, 0); + esp_rom_delay_us(100); // insert min input assert time + gpio_set_level(jl1101->reset_gpio_num, 1); + } + return ESP_OK; +} + +#if ESP_IDF_VERSION_MAJOR >= 5 +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) { +#else +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { +#endif + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* in case any link status has changed, let's assume we're in link down status */ + jl1101->link_status = ETH_LINK_DOWN; + /* Restart auto negotiation */ + bmcr_reg_t bmcr = { + .speed_select = 1, /* 100Mbps */ + .duplex_mode = 1, /* Full Duplex */ + .en_auto_nego = 1, /* Auto Negotiation */ + .restart_auto_nego = 1 /* Restart Auto Negotiation */ + }; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for auto negotiation complete */ + bmsr_reg_t bmsr; + uint32_t to = 0; + for (to = 0; to < jl1101->autonego_timeout_ms / 100; to++) { + vTaskDelay(pdMS_TO_TICKS(100)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + if (bmsr.auto_nego_complete) { + break; + } + } + /* Auto negotiation failed, maybe no network cable plugged in, so output a warning */ + if (to >= jl1101->autonego_timeout_ms / 100) { + ESP_LOGW(TAG, "auto negotiation timeout"); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_pwrctl(esp_eth_phy_t *phy, bool enable) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!enable) { + /* Enable IEEE Power Down Mode */ + bmcr.power_down = 1; + } else { + /* Disable IEEE Power Down Mode */ + bmcr.power_down = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + if (!enable) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + PHY_CHECK(bmcr.power_down == 1, "power down failed", err); + } else { + /* wait for power up complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 10; to++) { + vTaskDelay(pdMS_TO_TICKS(10)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.power_down == 0) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 10, "power up timeout", err); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_addr(esp_eth_phy_t *phy, uint32_t addr) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->addr = addr; + return ESP_OK; +} + +static esp_err_t jl1101_get_addr(esp_eth_phy_t *phy, uint32_t *addr) { + PHY_CHECK(addr, "addr can't be null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + *addr = jl1101->addr; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_del(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + free(jl1101); + return ESP_OK; +} + +static esp_err_t jl1101_advertise_pause_ability(esp_eth_phy_t *phy, uint32_t ability) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* Set PAUSE function ability */ + anar_reg_t anar; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, &(anar.val)) == ESP_OK, "read ANAR failed", + err); + if (ability) { + anar.asymmetric_pause = 1; + anar.symmetric_pause = 1; + } else { + anar.asymmetric_pause = 0; + anar.symmetric_pause = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, anar.val) == ESP_OK, "write ANAR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_init(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + // Detect PHY address + if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { +#if ESP_IDF_VERSION_MAJOR >= 5 + PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); +#else + PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); +#endif + } + /* Power on Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); + /* Reset Ethernet PHY */ + PHY_CHECK(jl1101_reset(phy) == ESP_OK, "reset failed", err); + /* Check PHY ID */ + phyidr1_reg_t id1; + phyidr2_reg_t id2; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR1_REG_ADDR, &(id1.val)) == ESP_OK, "read ID1 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR2_REG_ADDR, &(id2.val)) == ESP_OK, "read ID2 failed", err); + PHY_CHECK(id1.oui_msb == 0x937C && id2.oui_lsb == 0x10 && id2.vendor_model == 0x2, "wrong chip ID", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_deinit(esp_eth_phy_t *phy) { + /* Power off Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, false) == ESP_OK, "power control failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { + PHY_CHECK(config, "can't set phy config to null", err); + phy_jl1101_t *jl1101 = calloc(1, sizeof(phy_jl1101_t)); + PHY_CHECK(jl1101, "calloc jl1101 failed", err); + jl1101->addr = config->phy_addr; + jl1101->reset_gpio_num = config->reset_gpio_num; + jl1101->reset_timeout_ms = config->reset_timeout_ms; + jl1101->link_status = ETH_LINK_DOWN; + jl1101->autonego_timeout_ms = config->autonego_timeout_ms; + jl1101->parent.reset = jl1101_reset; + jl1101->parent.reset_hw = jl1101_reset_hw; + jl1101->parent.init = jl1101_init; + jl1101->parent.deinit = jl1101_deinit; + jl1101->parent.set_mediator = jl1101_set_mediator; +#if ESP_IDF_VERSION_MAJOR >= 5 + jl1101->parent.autonego_ctrl = jl1101_negotiate; +#else + jl1101->parent.negotiate = jl1101_negotiate; +#endif + jl1101->parent.get_link = jl1101_get_link; + jl1101->parent.pwrctl = jl1101_pwrctl; + jl1101->parent.get_addr = jl1101_get_addr; + jl1101->parent.set_addr = jl1101_set_addr; + jl1101->parent.advertise_pause_ability = jl1101_advertise_pause_ability; + jl1101->parent.del = jl1101_del; + + return &(jl1101->parent); +err: + return NULL; +} +#endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fa66d824be82..03fdc49338b7 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -26,8 +26,10 @@ EthernetComponent::EthernetComponent() { global_eth_component = this; } void EthernetComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Ethernet..."); - // Delay here to allow power to stabilise before Ethernet is initialised. - delay(300); // NOLINT + if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { + // Delay here to allow power to stabilise before Ethernet is initialized. + delay(300); // NOLINT + } esp_err_t err; err = esp_netif_init(); @@ -39,36 +41,56 @@ void EthernetComponent::setup() { this->eth_netif_ = esp_netif_new(&cfg); // Init MAC and PHY configs to default - eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); - phy_config.phy_addr = this->phy_addr_; - if (this->power_pin_ != -1) - phy_config.reset_gpio_num = this->power_pin_; + phy_config.reset_gpio_num = this->power_pin_; + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); +#if ESP_IDF_VERSION_MAJOR >= 5 + eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; + esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; + esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; + esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); +#else mac_config.smi_mdc_gpio_num = this->mdc_pin_; mac_config.smi_mdio_gpio_num = this->mdio_pin_; - mac_config.clock_config.rmii.clock_mode = this->clk_mode_ == EMAC_CLK_IN_GPIO ? EMAC_CLK_EXT_IN : EMAC_CLK_OUT; - mac_config.clock_config.rmii.clock_gpio = this->clk_mode_; + mac_config.clock_config.rmii.clock_mode = this->clk_mode_; + mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); +#endif - esp_eth_phy_t *phy; switch (this->type_) { case ETHERNET_TYPE_LAN8720: { - phy = esp_eth_phy_new_lan87xx(&phy_config); + this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); break; } case ETHERNET_TYPE_RTL8201: { - phy = esp_eth_phy_new_rtl8201(&phy_config); + this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); break; } case ETHERNET_TYPE_DP83848: { - phy = esp_eth_phy_new_dp83848(&phy_config); + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); break; } case ETHERNET_TYPE_IP101: { - phy = esp_eth_phy_new_ip101(&phy_config); + this->phy_ = esp_eth_phy_new_ip101(&phy_config); + break; + } + case ETHERNET_TYPE_JL1101: { + this->phy_ = esp_eth_phy_new_jl1101(&phy_config); + break; + } + case ETHERNET_TYPE_KSZ8081: + case ETHERNET_TYPE_KSZ8081RNA: { +#if ESP_IDF_VERSION_MAJOR >= 5 + this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); +#else + this->phy_ = esp_eth_phy_new_ksz8081(&phy_config); +#endif break; } default: { @@ -77,10 +99,16 @@ void EthernetComponent::setup() { } } - esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_); this->eth_handle_ = nullptr; err = esp_eth_driver_install(ð_config, &this->eth_handle_); ESPHL_ERROR_CHECK(err, "ETH driver install error"); + + if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { + // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. + this->ksz8081_set_clock_reference_(mac); + } + /* attach Ethernet driver to TCP/IP stack */ err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); ESPHL_ERROR_CHECK(err, "ETH netif attach error"); @@ -90,6 +118,10 @@ void EthernetComponent::setup() { ESPHL_ERROR_CHECK(err, "ETH event handler register error"); err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); +#if ENABLE_IPV6 + err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IP6 event handler register error"); +#endif /* ENABLE_IPV6 */ /* start Ethernet driver state machine */ err = esp_eth_start(this->eth_handle_); @@ -132,12 +164,26 @@ void EthernetComponent::loop() { this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); } +#if ENABLE_IPV6 + else if (this->got_ipv6_) { + esp_ip6_addr_t ip6_addr; + if (esp_netif_get_ip6_global(this->eth_netif_, &ip6_addr) == 0 && + esp_netif_ip6_get_addr_type(&ip6_addr) == ESP_IP6_ADDR_IS_GLOBAL) { + ESP_LOGCONFIG(TAG, "IPv6 Addr (Global): " IPV6STR, IPV62STR(ip6_addr)); + } else { + esp_netif_get_ip6_linklocal(this->eth_netif_, &ip6_addr); + ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(ip6_addr)); + } + + this->got_ipv6_ = false; + } +#endif /* ENABLE_IPV6 */ break; } } void EthernetComponent::dump_config() { - std::string eth_type; + const char *eth_type; switch (this->type_) { case ETHERNET_TYPE_LAN8720: eth_type = "LAN8720"; @@ -155,6 +201,18 @@ void EthernetComponent::dump_config() { eth_type = "IP101"; break; + case ETHERNET_TYPE_JL1101: + eth_type = "JL1101"; + break; + + case ETHERNET_TYPE_KSZ8081: + eth_type = "KSZ8081"; + break; + + case ETHERNET_TYPE_KSZ8081RNA: + eth_type = "KSZ8081RNA"; + break; + default: eth_type = "Unknown"; break; @@ -167,7 +225,8 @@ void EthernetComponent::dump_config() { } ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_); ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_); - ESP_LOGCONFIG(TAG, " Type: %s", eth_type.c_str()); + ESP_LOGCONFIG(TAG, " Type: %s", eth_type); + ESP_LOGCONFIG(TAG, " PHY addr: %u", this->phy_addr_); } float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } @@ -177,7 +236,7 @@ bool EthernetComponent::can_proceed() { return this->is_connected(); } network::IPAddress EthernetComponent::get_ip_address() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(this->eth_netif_, &ip); - return {ip.ip.addr}; + return network::IPAddress(&ip.ip); } void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { @@ -204,15 +263,24 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base return; } - ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); + ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); } void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { global_eth_component->connected_ = true; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%d)", event_id); + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%" PRId32 ")", event_id); } +#if ENABLE_IPV6 +void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP6 (num=%d)", event_id); + global_eth_component->got_ipv6_ = true; + global_eth_component->ipv6_count_ += 1; +} +#endif /* ENABLE_IPV6 */ + void EthernetComponent::start_connect_() { this->connect_begin_ = millis(); this->status_set_warning(); @@ -225,9 +293,9 @@ void EthernetComponent::start_connect_() { esp_netif_ip_info_t info; if (this->manual_ip_.has_value()) { - info.ip.addr = static_cast(this->manual_ip_->static_ip); - info.gw.addr = static_cast(this->manual_ip_->gateway); - info.netmask.addr = static_cast(this->manual_ip_->subnet); + info.ip = this->manual_ip_->static_ip; + info.gw = this->manual_ip_->gateway; + info.netmask = this->manual_ip_->subnet; } else { info.ip.addr = 0; info.gw.addr = 0; @@ -250,16 +318,14 @@ void EthernetComponent::start_connect_() { ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); if (this->manual_ip_.has_value()) { - if (uint32_t(this->manual_ip_->dns1) != 0) { + if (this->manual_ip_->dns1.is_set()) { ip_addr_t d; - d.type = IPADDR_TYPE_V4; - d.u_addr.ip4.addr = static_cast(this->manual_ip_->dns1); + d = this->manual_ip_->dns1; dns_setserver(0, &d); } - if (uint32_t(this->manual_ip_->dns1) != 0) { + if (this->manual_ip_->dns2.is_set()) { ip_addr_t d; - d.type = IPADDR_TYPE_V4; - d.u_addr.ip4.addr = static_cast(this->manual_ip_->dns2); + d = this->manual_ip_->dns2; dns_setserver(1, &d); } } else { @@ -267,6 +333,12 @@ void EthernetComponent::start_connect_() { if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); } +#if ENABLE_IPV6 + err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err != ESP_OK) { + ESPHL_ERROR_CHECK(err, "IPv6 local failed"); + } +#endif /* ENABLE_IPV6 */ } this->connect_begin_ = millis(); @@ -278,16 +350,29 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen void EthernetComponent::dump_connect_params_() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(this->eth_netif_, &ip); - ESP_LOGCONFIG(TAG, " IP Address: %s", network::IPAddress(ip.ip.addr).str().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", network::IPAddress(&ip.ip).str().c_str()); ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); - ESP_LOGCONFIG(TAG, " Subnet: %s", network::IPAddress(ip.netmask.addr).str().c_str()); - ESP_LOGCONFIG(TAG, " Gateway: %s", network::IPAddress(ip.gw.addr).str().c_str()); + ESP_LOGCONFIG(TAG, " Subnet: %s", network::IPAddress(&ip.netmask).str().c_str()); + ESP_LOGCONFIG(TAG, " Gateway: %s", network::IPAddress(&ip.gw).str().c_str()); const ip_addr_t *dns_ip1 = dns_getserver(0); const ip_addr_t *dns_ip2 = dns_getserver(1); - ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1->u_addr.ip4.addr).str().c_str()); - ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2->u_addr.ip4.addr).str().c_str()); + ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1).str().c_str()); + ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2).str().c_str()); + +#if ENABLE_IPV6 + if (this->ipv6_count_ > 0) { + esp_ip6_addr_t ip6_addr; + esp_netif_get_ip6_linklocal(this->eth_netif_, &ip6_addr); + ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(ip6_addr)); + + if (esp_netif_get_ip6_global(this->eth_netif_, &ip6_addr) == 0 && + esp_netif_ip6_get_addr_type(&ip6_addr) == ESP_IP6_ADDR_IS_GLOBAL) { + ESP_LOGCONFIG(TAG, "IPv6 Addr (Global): " IPV6STR, IPV62STR(ip6_addr)); + } + } +#endif /* ENABLE_IPV6 */ esp_err_t err; @@ -312,7 +397,10 @@ void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_ void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_gpio_t clk_mode) { this->clk_mode_ = clk_mode; } +void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { + this->clk_mode_ = clk_mode; + this->clk_gpio_ = clk_gpio; +} void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } std::string EthernetComponent::get_use_address() const { @@ -324,6 +412,52 @@ std::string EthernetComponent::get_use_address() const { void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +bool EthernetComponent::powerdown() { + ESP_LOGI(TAG, "Powering down ethernet PHY"); + if (this->phy_ == nullptr) { + ESP_LOGE(TAG, "Ethernet PHY not assigned"); + return false; + } + this->connected_ = false; + this->started_ = false; + if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { + ESP_LOGE(TAG, "Error powering down ethernet PHY"); + return false; + } + return true; +} + +void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { +#define KSZ80XX_PC2R_REG_ADDR (0x1F) + + esp_err_t err; + + uint32_t phy_control_2; + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + + /* + * Bit 7 is `RMII Reference Clock Select`. Default is `0`. + * KSZ8081RNA: + * 0 - clock input to XI (Pin 8) is 25 MHz for RMII – 25 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode. + * KSZ8081RND: + * 0 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII – 25 MHz clock mode. + */ + if ((phy_control_2 & (1 << 7)) != (1 << 7)) { + phy_control_2 |= 1 << 7; + err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); + ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + } + +#undef KSZ80XX_PC2R_REG_ADDR +} + } // namespace ethernet } // namespace esphome diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index ed784e1bc0a5..11f50af966f0 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/components/network/ip_address.h" @@ -14,10 +15,14 @@ namespace esphome { namespace ethernet { enum EthernetType { - ETHERNET_TYPE_LAN8720 = 0, + ETHERNET_TYPE_UNKNOWN = 0, + ETHERNET_TYPE_LAN8720, ETHERNET_TYPE_RTL8201, ETHERNET_TYPE_DP83848, ETHERNET_TYPE_IP101, + ETHERNET_TYPE_JL1101, + ETHERNET_TYPE_KSZ8081, + ETHERNET_TYPE_KSZ8081RNA, }; struct ManualIP { @@ -42,6 +47,7 @@ class EthernetComponent : public Component { void dump_config() override; float get_setup_priority() const override; bool can_proceed() override; + void on_shutdown() override { powerdown(); } bool is_connected(); void set_phy_addr(uint8_t phy_addr); @@ -49,39 +55,52 @@ class EthernetComponent : public Component { void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); void set_type(EthernetType type); - void set_clk_mode(emac_rmii_clock_gpio_t clk_mode); + void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); void set_manual_ip(const ManualIP &manual_ip); network::IPAddress get_ip_address(); std::string get_use_address() const; void set_use_address(const std::string &use_address); + bool powerdown(); protected: static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +#if LWIP_IPV6 + static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +#endif /* LWIP_IPV6 */ void start_connect_(); void dump_connect_params_(); + /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. + void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); std::string use_address_; uint8_t phy_addr_{0}; int power_pin_{-1}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; - EthernetType type_{ETHERNET_TYPE_LAN8720}; - emac_rmii_clock_gpio_t clk_mode_{EMAC_CLK_IN_GPIO}; + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; + emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; + emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; optional manual_ip_{}; bool started_{false}; bool connected_{false}; +#if LWIP_IPV6 + bool got_ipv6_{false}; + uint8_t ipv6_count_{0}; +#endif /* LWIP_IPV6 */ EthernetComponentState state_{EthernetComponentState::STOPPED}; uint32_t connect_begin_; esp_netif_t *eth_netif_{nullptr}; esp_eth_handle_t eth_handle_; + esp_eth_phy_t *phy_{nullptr}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; +extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); } // namespace ethernet } // namespace esphome diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index e69872c290d8..f84187539648 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -1,7 +1,7 @@ #include "ethernet_info_text_sensor.h" #include "esphome/core/log.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 namespace esphome { namespace ethernet_info { @@ -13,4 +13,4 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP } // namespace ethernet_info } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index aad8f362b5f2..2d46fe18ebb6 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -4,7 +4,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/ethernet/ethernet_component.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 namespace esphome { namespace ethernet_info { @@ -30,4 +30,4 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS } // namespace ethernet_info } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 53fd337ed8a6..bbb703dc5cee 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -1,90 +1,32 @@ -import re import logging from pathlib import Path import esphome.config_validation as cv +from esphome import git, loader from esphome.const import ( CONF_COMPONENTS, + CONF_EXTERNAL_COMPONENTS, + CONF_PASSWORD, + CONF_PATH, CONF_REF, CONF_REFRESH, CONF_SOURCE, - CONF_URL, CONF_TYPE, - CONF_EXTERNAL_COMPONENTS, - CONF_PATH, + CONF_URL, CONF_USERNAME, - CONF_PASSWORD, + TYPE_GIT, + TYPE_LOCAL, ) from esphome.core import CORE -from esphome import git, loader _LOGGER = logging.getLogger(__name__) DOMAIN = CONF_EXTERNAL_COMPONENTS -TYPE_GIT = "git" -TYPE_LOCAL = "local" - - -GIT_SCHEMA = { - cv.Required(CONF_URL): cv.url, - cv.Optional(CONF_REF): cv.git_ref, - cv.Optional(CONF_USERNAME): cv.string, - cv.Optional(CONF_PASSWORD): cv.string, -} -LOCAL_SCHEMA = { - cv.Required(CONF_PATH): cv.directory, -} - - -def validate_source_shorthand(value): - if not isinstance(value, str): - raise cv.Invalid("Shorthand only for strings") - try: - return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) - except cv.Invalid: - pass - # Regex for GitHub repo name with optional branch/tag - # Note: git allows other branch/tag names as well, but never seen them used before - m = re.match( - r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))", - value, - ) - if m is None: - raise cv.Invalid( - "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" - ) - if m.group(4): - conf = { - CONF_TYPE: TYPE_GIT, - CONF_URL: "https://github.com/esphome/esphome.git", - CONF_REF: f"pull/{m.group(4)}/head", - } - else: - conf = { - CONF_TYPE: TYPE_GIT, - CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", - } - if m.group(3): - conf[CONF_REF] = m.group(3) - - return SOURCE_SCHEMA(conf) - - -SOURCE_SCHEMA = cv.Any( - validate_source_shorthand, - cv.typed_schema( - { - TYPE_GIT: cv.Schema(GIT_SCHEMA), - TYPE_LOCAL: cv.Schema(LOCAL_SCHEMA), - } - ), -) - CONFIG_SCHEMA = cv.ensure_list( { - cv.Required(CONF_SOURCE): SOURCE_SCHEMA, + cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( "all", cv.ensure_list(cv.string) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 9d4343e00464..8e4486dbf253 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -106,20 +106,18 @@ void EZOSensor::loop() { break; } - ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", buf, EZO_COMMAND_TYPE_STRINGS[to_run->command_type]); + ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]); - if ((buf[0] == 1) || (to_run->command_type == EzoCommandType::EZO_CALIBRATION)) { // EZO_CALIBRATION returns 0-3 - // some sensors return multiple comma-separated values, terminate string after first one - for (size_t i = 1; i < sizeof(buf) - 1; i++) { - if (buf[i] == ',') { - buf[i] = '\0'; - break; - } - } + if (buf[0] == 1) { std::string payload = reinterpret_cast(&buf[1]); if (!payload.empty()) { switch (to_run->command_type) { case EzoCommandType::EZO_READ: { + // some sensors return multiple comma-separated values, terminate string after first one + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + payload.erase(start_location); + } auto val = parse_number(payload); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); @@ -154,7 +152,10 @@ void EZOSensor::loop() { break; } case EzoCommandType::EZO_T: { - this->t_callback_.call(payload); + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->t_callback_.call(payload.substr(start_location + 1)); + } break; } case EzoCommandType::EZO_CUSTOM: { diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index 72e82a574f02..28b46643e9d5 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -12,14 +12,14 @@ static const char *const TAG = "ezo.sensor"; enum EzoCommandType : uint8_t { EZO_READ = 0, - EZO_LED = 1, - EZO_DEVICE_INFORMATION = 2, - EZO_SLOPE = 3, + EZO_LED, + EZO_DEVICE_INFORMATION, + EZO_SLOPE, EZO_CALIBRATION, - EZO_SLEEP = 4, - EZO_I2C = 5, - EZO_T = 6, - EZO_CUSTOM = 7 + EZO_SLEEP, + EZO_I2C, + EZO_T, + EZO_CUSTOM }; enum EzoCalibrationType : uint8_t { EZO_CAL_LOW = 0, EZO_CAL_MID = 1, EZO_CAL_HIGH = 2 }; diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index 049b1bf4d0c3..486ba0126e01 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -41,9 +41,9 @@ LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_)) CONFIG_SCHEMA = ( - sensor.SENSOR_SCHEMA.extend( + sensor.sensor_schema(EZOSensor) + .extend( { - cv.GenerateID(): cv.declare_id(EZOSensor), cv.Optional(CONF_ON_CUSTOM): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger), diff --git a/esphome/components/factory_reset/button/__init__.py b/esphome/components/factory_reset/button/__init__.py index d5beac34b53d..010691ac7f14 100644 --- a/esphome/components/factory_reset/button/__init__.py +++ b/esphome/components/factory_reset/button/__init__.py @@ -13,15 +13,12 @@ "FactoryResetButton", 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(FactoryResetButton)}) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = button.button_schema( + FactoryResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index eb67bbcbd76c..23df3c2214b9 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -14,6 +14,7 @@ CONF_SPEED_LEVEL_STATE_TOPIC, CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, + CONF_OFF_SPEED_CYCLE, CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, @@ -63,7 +64,7 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(Fan), - cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), @@ -217,10 +218,22 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): return var -@automation.register_action("fan.cycle_speed", CycleSpeedAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.cycle_speed", + CycleSpeedAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Fan), + cv.Optional(CONF_OFF_SPEED_CYCLE, default=True): cv.boolean, + } + ), +) async def fan_cycle_speed_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) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, bool) + cg.add(var.set_no_off_cycle(template_)) + return var @automation.register_condition( diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 23fb70a95b1c..511acf5682bf 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -54,18 +54,26 @@ template class CycleSpeedAction : public Action { public: explicit CycleSpeedAction(Fan *state) : state_(state) {} + TEMPLATABLE_VALUE(bool, no_off_cycle) + void play(Ts... x) override { // check to see if fan supports speeds and is on if (this->state_->get_traits().supported_speed_count()) { if (this->state_->state) { int speed = this->state_->speed + 1; int supported_speed_count = this->state_->get_traits().supported_speed_count(); - if (speed > supported_speed_count) { - // was running at max speed, so turn off + bool off_speed_cycle = no_off_cycle_.value(x...); + if (speed > supported_speed_count && off_speed_cycle) { + // was running at max speed, off speed cycle enabled, so turn off speed = 1; auto call = this->state_->turn_off(); call.set_speed(speed); call.perform(); + } else if (speed > supported_speed_count && !off_speed_cycle) { + // was running at max speed, off speed cycle disabled, so set to lowest speed + auto call = this->state_->turn_on(); + call.set_speed(1); + call.perform(); } else { auto call = this->state_->turn_on(); call.set_speed(speed); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index c8a3064f6cd5..87566bad4a1e 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -80,9 +80,6 @@ void FanRestoreState::apply(Fan &fan) { fan.publish_state(); } -Fan::Fan() : EntityBase("") {} -Fan::Fan(const std::string &name) : EntityBase(name) {} - FanCall Fan::turn_on() { return this->make_call().set_state(true); } FanCall Fan::turn_off() { return this->make_call().set_state(false); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 337bb3935a5e..f9d317e6759c 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -99,10 +99,6 @@ struct FanRestoreState { class Fan : public EntityBase { public: - Fan(); - /// Construct the fan with name. - explicit Fan(const std::string &name); - /// The current on/off state of the fan. bool state{false}; /// The current oscillation state of the fan. diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 044ee5973606..5926e700b0fd 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -15,7 +15,6 @@ enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component { public: FanState() = default; - explicit FanState(const std::string &name) : Fan(name) {} /// Get the traits of this fan. FanTraits get_traits() override { return this->traits_; } diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index 213ce7ff8f35..117c626f582c 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -41,6 +41,7 @@ void FeedbackCover::setup() { CoverTraits FeedbackCover::get_traits() { auto traits = CoverTraits(); + traits.set_supports_stop(true); traits.set_supports_position(true); traits.set_supports_toggle(true); traits.set_is_assumed_state(this->assumed_state_); diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 1d6cb776b736..4043f32dcb86 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -77,10 +77,12 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) { this->enrollment_done_callback_.call(this->enrollment_slot_); this->get_fingerprint_count_(); } else { - this->enrollment_failed_callback_.call(this->enrollment_slot_); + if (this->enrollment_slot_ != ENROLLMENT_SLOT_UNUSED) { + this->enrollment_failed_callback_.call(this->enrollment_slot_); + } } this->enrollment_image_ = 0; - this->enrollment_slot_ = 0; + this->enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; if (this->enrolling_binary_sensor_ != nullptr) { this->enrolling_binary_sensor_->publish_state(false); } @@ -95,7 +97,7 @@ void FingerprintGrowComponent::scan_and_match_() { } if (this->scan_image_(1) == OK) { this->waiting_removal_ = true; - this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t)(this->capacity_ >> 8), (uint8_t)(this->capacity_ & 0xFF)}; + this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t) (this->capacity_ >> 8), (uint8_t) (this->capacity_ & 0xFF)}; switch (this->send_command_()) { case OK: { ESP_LOGD(TAG, "Fingerprint matched"); @@ -171,7 +173,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { } ESP_LOGI(TAG, "Storing model"); - this->data_ = {STORE, 0x01, (uint8_t)(this->enrollment_slot_ >> 8), (uint8_t)(this->enrollment_slot_ & 0xFF)}; + this->data_ = {STORE, 0x01, (uint8_t) (this->enrollment_slot_ >> 8), (uint8_t) (this->enrollment_slot_ & 0xFF)}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Stored model"); @@ -188,8 +190,8 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { bool FingerprintGrowComponent::check_password_() { ESP_LOGD(TAG, "Checking password"); - this->data_ = {VERIFY_PASSWORD, (uint8_t)(this->password_ >> 24), (uint8_t)(this->password_ >> 16), - (uint8_t)(this->password_ >> 8), (uint8_t)(this->password_ & 0xFF)}; + this->data_ = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), + (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; switch (this->send_command_()) { case OK: ESP_LOGD(TAG, "Password verified"); @@ -203,8 +205,8 @@ bool FingerprintGrowComponent::check_password_() { bool FingerprintGrowComponent::set_password_() { ESP_LOGI(TAG, "Setting new password: %d", this->new_password_); - this->data_ = {SET_PASSWORD, (uint8_t)(this->new_password_ >> 24), (uint8_t)(this->new_password_ >> 16), - (uint8_t)(this->new_password_ >> 8), (uint8_t)(this->new_password_ & 0xFF)}; + this->data_ = {SET_PASSWORD, (uint8_t) (this->new_password_ >> 24), (uint8_t) (this->new_password_ >> 16), + (uint8_t) (this->new_password_ >> 8), (uint8_t) (this->new_password_ & 0xFF)}; if (this->send_command_() == OK) { ESP_LOGI(TAG, "New password successfully set"); ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); @@ -250,7 +252,7 @@ void FingerprintGrowComponent::get_fingerprint_count_() { void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { ESP_LOGI(TAG, "Deleting fingerprint in slot %d", finger_id); - this->data_ = {DELETE, (uint8_t)(finger_id >> 8), (uint8_t)(finger_id & 0xFF), 0x00, 0x01}; + this->data_ = {DELETE, (uint8_t) (finger_id >> 8), (uint8_t) (finger_id & 0xFF), 0x00, 0x01}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Deleted fingerprint"); @@ -320,8 +322,8 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } uint8_t FingerprintGrowComponent::send_command_() { - this->write((uint8_t)(START_CODE >> 8)); - this->write((uint8_t)(START_CODE & 0xFF)); + this->write((uint8_t) (START_CODE >> 8)); + this->write((uint8_t) (START_CODE & 0xFF)); this->write(this->address_[0]); this->write(this->address_[1]); this->write(this->address_[2]); @@ -329,8 +331,8 @@ uint8_t FingerprintGrowComponent::send_command_() { this->write(COMMAND); uint16_t wire_length = this->data_.size() + 2; - this->write((uint8_t)(wire_length >> 8)); - this->write((uint8_t)(wire_length & 0xFF)); + this->write((uint8_t) (wire_length >> 8)); + this->write((uint8_t) (wire_length & 0xFF)); uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; for (auto data : this->data_) { @@ -338,8 +340,8 @@ uint8_t FingerprintGrowComponent::send_command_() { sum += data; } - this->write((uint8_t)(sum >> 8)); - this->write((uint8_t)(sum & 0xFF)); + this->write((uint8_t) (sum >> 8)); + this->write((uint8_t) (sum & 0xFF)); this->data_.clear(); @@ -354,11 +356,11 @@ uint8_t FingerprintGrowComponent::send_command_() { byte = this->read(); switch (idx) { case 0: - if (byte != (uint8_t)(START_CODE >> 8)) + if (byte != (uint8_t) (START_CODE >> 8)) continue; break; case 1: - if (byte != (uint8_t)(START_CODE & 0xFF)) { + if (byte != (uint8_t) (START_CODE & 0xFF)) { idx = 0; continue; } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 96d02a1e8cf1..f414146e64ef 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -13,6 +13,8 @@ namespace fingerprint_grow { static const uint16_t START_CODE = 0xEF01; +static const uint16_t ENROLLMENT_SLOT_UNUSED = 0xFFFF; + enum GrowPacketType { COMMAND = 0x01, DATA = 0x02, @@ -91,10 +93,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void dump_config() override; void set_address(uint32_t address) { - this->address_[0] = (uint8_t)(address >> 24); - this->address_[1] = (uint8_t)(address >> 16); - this->address_[2] = (uint8_t)(address >> 8); - this->address_[3] = (uint8_t)(address & 0xFF); + this->address_[0] = (uint8_t) (address >> 24); + this->address_[1] = (uint8_t) (address >> 16); + this->address_[2] = (uint8_t) (address >> 8); + this->address_[3] = (uint8_t) (address & 0xFF); } void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } void set_password(uint32_t password) { this->password_ = password; } @@ -158,7 +160,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint32_t new_password_ = -1; GPIOPin *sensing_pin_{nullptr}; uint8_t enrollment_image_ = 0; - uint16_t enrollment_slot_ = 0; + uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint8_t enrollment_buffers_ = 5; bool waiting_removal_ = false; uint32_t last_aura_led_control_ = 0; diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index aa165ebaa5a2..2bd6beeaeb08 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -3,11 +3,11 @@ import hashlib import os import re +from packaging import version import requests from esphome import core -from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg from esphome.helpers import copy_file_if_changed @@ -29,9 +29,11 @@ DEPENDENCIES = ["display"] MULTI_CONF = True -Font = display.display_ns.class_("Font") -Glyph = display.display_ns.class_("Glyph") -GlyphData = display.display_ns.struct("GlyphData") +font_ns = cg.esphome_ns.namespace("font") + +Font = font_ns.class_("Font") +Glyph = font_ns.class_("Glyph") +GlyphData = font_ns.struct("GlyphData") def validate_glyphs(value): @@ -65,13 +67,13 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - "(pip install pillow)" + '(pip install "pillow==10.0.1")' ) from err - if PIL.__version__[0] < "4": + if version.parse(PIL.__version__) != version.parse("10.0.1"): raise cv.Invalid( - "Please update your pillow installation to at least 4.0.x. " - "(pip install -U pillow)" + "Please update your pillow installation to 10.0.1. " + '(pip install "pillow==10.0.1")' ) return value @@ -91,10 +93,9 @@ def validate_truetype_file(value): def _compute_local_font_dir(name) -> Path: - base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN h = hashlib.new("sha256") h.update(name.encode()) - return base_dir / h.hexdigest()[:8] + return Path(CORE.data_dir) / DOMAIN / h.hexdigest()[:8] def _compute_gfonts_local_path(value) -> Path: diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp new file mode 100644 index 000000000000..ef5b2b788df8 --- /dev/null +++ b/esphome/components/font/font.cpp @@ -0,0 +1,149 @@ +#include "font.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace font { + +static const char *const TAG = "font"; + +void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const { + int scan_x1, scan_y1, scan_width, scan_height; + this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); + + const unsigned char *data = this->glyph_data_->data; + const int max_x = x_at + scan_x1 + scan_width; + const int max_y = y_start + scan_y1 + scan_height; + + for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) { + for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) { + uint8_t pixel_data = progmem_read_byte(data); + const int pixel_max_x = std::min(max_x, glyph_x + 8); + + for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) { + if (pixel_data & 0x80) { + display->draw_pixel_at(pixel_x, glyph_y, color); + } + } + } + } +} +const char *Glyph::get_char() const { return this->glyph_data_->a_char; } +bool Glyph::compare_to(const char *str) const { + // 1 -> this->char_ + // 2 -> str + for (uint32_t i = 0;; i++) { + if (this->glyph_data_->a_char[i] == '\0') + return true; + if (str[i] == '\0') + return false; + if (this->glyph_data_->a_char[i] > str[i]) + return false; + if (this->glyph_data_->a_char[i] < str[i]) + return true; + } + // this should not happen + return false; +} +int Glyph::match_length(const char *str) const { + for (uint32_t i = 0;; i++) { + if (this->glyph_data_->a_char[i] == '\0') + return i; + if (str[i] != this->glyph_data_->a_char[i]) + return 0; + } + // this should not happen + return 0; +} +void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { + *x1 = this->glyph_data_->offset_x; + *y1 = this->glyph_data_->offset_y; + *width = this->glyph_data_->width; + *height = this->glyph_data_->height; +} + +Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { + glyphs_.reserve(data_nr); + for (int i = 0; i < data_nr; ++i) + glyphs_.emplace_back(&data[i]); +} +int Font::match_next_glyph(const char *str, int *match_length) { + int lo = 0; + int hi = this->glyphs_.size() - 1; + while (lo != hi) { + int mid = (lo + hi + 1) / 2; + if (this->glyphs_[mid].compare_to(str)) { + lo = mid; + } else { + hi = mid - 1; + } + } + *match_length = this->glyphs_[lo].match_length(str); + if (*match_length <= 0) + return -1; + return lo; +} +void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { + *baseline = this->baseline_; + *height = this->height_; + int i = 0; + int min_x = 0; + bool has_char = false; + int x = 0; + while (str[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph(str + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + if (!this->get_glyphs().empty()) + x += this->get_glyphs()[0].glyph_data_->width; + i++; + continue; + } + + const Glyph &glyph = this->glyphs_[glyph_n]; + if (!has_char) { + min_x = glyph.glyph_data_->offset_x; + } else { + min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + } + x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + has_char = true; + } + *x_offset = min_x; + *width = x - min_x; +} +void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) { + int i = 0; + int x_at = x_start; + while (text[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph(text + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); + if (!this->get_glyphs().empty()) { + uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width; + display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + x_at += glyph_width; + } + + i++; + continue; + } + + const Glyph &glyph = this->get_glyphs()[glyph_n]; + glyph.draw(x_at, y_start, display, color); + x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + } +} + +} // namespace font +} // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h new file mode 100644 index 000000000000..03171a61263e --- /dev/null +++ b/esphome/components/font/font.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/datatypes.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace font { + +class Font; + +struct GlyphData { + const char *a_char; + const uint8_t *data; + int offset_x; + int offset_y; + int width; + int height; +}; + +class Glyph { + public: + Glyph(const GlyphData *data) : glyph_data_(data) {} + + void draw(int x, int y, display::Display *display, Color color) const; + + const char *get_char() const; + + bool compare_to(const char *str) const; + + int match_length(const char *str) const; + + void scan_area(int *x1, int *y1, int *width, int *height) const; + + protected: + friend Font; + + const GlyphData *glyph_data_; +}; + +class Font : public display::BaseFont { + public: + /** Construct the font with the given glyphs. + * + * @param glyphs A vector of glyphs, must be sorted lexicographically. + * @param baseline The y-offset from the top of the text to the baseline. + * @param bottom The y-offset from the top of the text to the bottom (i.e. height). + */ + Font(const GlyphData *data, int data_nr, int baseline, int height); + + int match_next_glyph(const char *str, int *match_length); + + void print(int x_start, int y_start, display::Display *display, Color color, const char *text) override; + void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; + inline int get_baseline() { return this->baseline_; } + inline int get_height() { return this->height_; } + + const std::vector> &get_glyphs() const { return glyphs_; } + + protected: + std::vector> glyphs_; + int baseline_; + int height_; +}; + +} // namespace font +} // namespace esphome diff --git a/esphome/components/fs3000/__init__.py b/esphome/components/fs3000/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/esphome/components/fs3000/fs3000.cpp b/esphome/components/fs3000/fs3000.cpp new file mode 100644 index 000000000000..fb729ed0a096 --- /dev/null +++ b/esphome/components/fs3000/fs3000.cpp @@ -0,0 +1,107 @@ +#include "fs3000.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace fs3000 { + +static const char *const TAG = "fs3000"; + +void FS3000Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up FS3000..."); + + if (model_ == FIVE) { + // datasheet gives 9 points to interpolate from for the 1005 model + static const uint16_t RAW_DATA_POINTS_1005[9] = {409, 915, 1522, 2066, 2523, 2908, 3256, 3572, 3686}; + static const float MPS_DATA_POINTS_1005[9] = {0.0, 1.07, 2.01, 3.0, 3.97, 4.96, 5.98, 6.99, 7.23}; + + std::copy(RAW_DATA_POINTS_1005, RAW_DATA_POINTS_1005 + 9, this->raw_data_points_); + std::copy(MPS_DATA_POINTS_1005, MPS_DATA_POINTS_1005 + 9, this->mps_data_points_); + } else if (model_ == FIFTEEN) { + // datasheet gives 13 points to extrapolate from for the 1015 model + static const uint16_t RAW_DATA_POINTS_1015[13] = {409, 1203, 1597, 1908, 2187, 2400, 2629, + 2801, 3006, 3178, 3309, 3563, 3686}; + static const float MPS_DATA_POINTS_1015[13] = {0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 13.0, 15.0}; + + std::copy(RAW_DATA_POINTS_1015, RAW_DATA_POINTS_1015 + 13, this->raw_data_points_); + std::copy(MPS_DATA_POINTS_1015, MPS_DATA_POINTS_1015 + 13, this->mps_data_points_); + } +} + +void FS3000Component::update() { + // 5 bytes of data read from fs3000 sensor + // byte 1 - checksum + // byte 2 - (lower 4 bits) high byte of sensor reading + // byte 3 - (8 bits) low byte of sensor reading + // byte 4 - generic checksum data + // byte 5 - generic checksum data + + uint8_t data[5]; + + if (!this->read_bytes_raw(data, 5)) { + this->status_set_warning(); + ESP_LOGW(TAG, "Error reading data from FS3000"); + this->publish_state(NAN); + return; + } + + // checksum passes if the modulo 256 sum of the five bytes is 0 + uint8_t checksum = 0; + for (uint8_t i : data) { + checksum += i; + } + + if (checksum != 0) { + this->status_set_warning(); + ESP_LOGW(TAG, "Checksum failure when reading from FS3000"); + return; + } + + // raw value information is 12 bits + uint16_t raw_value = (data[1] << 8) | data[2]; + ESP_LOGV(TAG, "Got raw reading=%i", raw_value); + + // convert and publish the raw value into m/s using the table of data points in the datasheet + this->publish_state(fit_raw_(raw_value)); + + this->status_clear_warning(); +} + +void FS3000Component::dump_config() { + ESP_LOGCONFIG(TAG, "FS3000:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Air Velocity", this); +} + +float FS3000Component::fit_raw_(uint16_t raw_value) { + // converts a raw value read from the FS3000 into a speed in m/s based on the + // reference data points given in the datasheet + // fits raw reading using a linear interpolation between each data point + + uint8_t end = 8; // assume model 1005, which has 9 data points + if (this->model_ == FIFTEEN) + end = 12; // model 1015 has 13 data points + + if (raw_value <= this->raw_data_points_[0]) { // less than smallest data point returns first data point + return this->mps_data_points_[0]; + } else if (raw_value >= this->raw_data_points_[end]) { // greater than largest data point returns max speed + return this->mps_data_points_[end]; + } else { + uint8_t i = 0; + + // determine between which data points does the reading fall, i-1 and i + while (raw_value > this->raw_data_points_[i]) { + ++i; + } + + // calculate the slope of the secant line between the two data points that surrounds the reading + float slope = (this->mps_data_points_[i] - this->mps_data_points_[i - 1]) / + (this->raw_data_points_[i] - this->raw_data_points_[i - 1]); + + // return the interpolated value for the reading + return (float(raw_value - this->raw_data_points_[i - 1])) * slope + this->mps_data_points_[i - 1]; + } +} + +} // namespace fs3000 +} // namespace esphome diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h new file mode 100644 index 000000000000..be3680e7e19d --- /dev/null +++ b/esphome/components/fs3000/fs3000.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace fs3000 { + +// FS3000 has two models, 1005 and 1015 +// 1005 has a max speed detection of 7.23 m/s +// 1015 has a max speed detection of 15 m/s +enum FS3000Model { FIVE, FIFTEEN }; + +class FS3000Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void setup() override; + void update() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_model(FS3000Model model) { this->model_ = model; } + + protected: + FS3000Model model_{}; + + uint16_t raw_data_points_[13]; + float mps_data_points_[13]; + + float fit_raw_(uint16_t raw_value); +}; + +} // namespace fs3000 +} // namespace esphome diff --git a/esphome/components/fs3000/sensor.py b/esphome/components/fs3000/sensor.py new file mode 100644 index 000000000000..0c50f5297924 --- /dev/null +++ b/esphome/components/fs3000/sensor.py @@ -0,0 +1,50 @@ +# initially based off of TMP117 component + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_MODEL, + DEVICE_CLASS_WIND_SPEED, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +fs3000_ns = cg.esphome_ns.namespace("fs3000") + +FS3000Model = fs3000_ns.enum("MODEL") +FS3000_MODEL_OPTIONS = { + "1005": FS3000Model.FIVE, + "1015": FS3000Model.FIFTEEN, +} + +FS3000Component = fs3000_ns.class_( + "FS3000Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + FS3000Component, + unit_of_measurement="m/s", + accuracy_decimals=2, + device_class=DEVICE_CLASS_WIND_SPEED, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Required(CONF_MODEL): cv.enum(FS3000_MODEL_OPTIONS, lower=True), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 291af8c8cd6d..6c7adebfeafb 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() { case climate::CLIMATE_FAN_LOW: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); break; + case climate::CLIMATE_FAN_QUIET: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT); + break; case climate::CLIMATE_FAN_AUTO: default: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); break; - // TODO Quiet / Silent } // Set swing @@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); switch (recv_fan_mode) { - // TODO No Quiet / Silent in ESPH case FUJITSU_GENERAL_FAN_SILENT: + this->fan_mode = climate::CLIMATE_FAN_QUIET; + break; case FUJITSU_GENERAL_FAN_LOW: this->fan_mode = climate::CLIMATE_FAN_LOW; break; diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index ee83ae9d19d4..d7d01bf6f3e1 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH}, + climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} diff --git a/esphome/components/gcja5/__init__.py b/esphome/components/gcja5/__init__.py new file mode 100644 index 000000000000..122ffaf6edf8 --- /dev/null +++ b/esphome/components/gcja5/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gcormier"] diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp new file mode 100644 index 000000000000..7f980ca0ad37 --- /dev/null +++ b/esphome/components/gcja5/gcja5.cpp @@ -0,0 +1,119 @@ +/* From snooping with a logic analyzer, the I2C on this sensor is broken. I was only able + * to receive 1's as a response from the sensor. I was able to get the UART working. + * + * The datasheet says the values should be divided by 1000, but this must only be for the I2C + * implementation. Comparing UART values with another sensor, there is no need to divide by 1000. + */ +#include "gcja5.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace gcja5 { + +static const char *const TAG = "gcja5"; + +void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } + +void GCJA5Component::loop() { + const uint32_t now = millis(); + if (now - this->last_transmission_ >= 500) { + // last transmission too long ago. Reset RX index. + this->rx_message_.clear(); + } + + if (this->available() == 0) { + return; + } + + // There must now be data waiting + this->last_transmission_ = now; + uint8_t val; + while (this->available() != 0) { + this->read_byte(&val); + this->rx_message_.push_back(val); + + // check if rx_message_ has 32 bytes of data + if (this->rx_message_.size() == 32) { + this->parse_data_(); + + if (this->have_good_data_) { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(get_32_bit_uint_(1)); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(get_32_bit_uint_(5)); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(get_32_bit_uint_(9)); + if (this->pmc_0_3_sensor_ != nullptr) + this->pmc_0_3_sensor_->publish_state(get_16_bit_uint_(13)); + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(get_16_bit_uint_(15)); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(get_16_bit_uint_(17)); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(get_16_bit_uint_(21)); + if (this->pmc_5_0_sensor_ != nullptr) + this->pmc_5_0_sensor_->publish_state(get_16_bit_uint_(23)); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(get_16_bit_uint_(25)); + } else { + this->status_set_warning(); + ESP_LOGV(TAG, "Have 32 bytes but not good data. Skipping."); + } + + this->rx_message_.clear(); + } + } +} + +bool GCJA5Component::calculate_checksum_() { + uint8_t crc = 0; + + for (uint8_t i = 1; i < 30; i++) + crc = crc ^ this->rx_message_[i]; + + ESP_LOGVV(TAG, "Checksum packet was (0x%02X), calculated checksum was (0x%02X)", this->rx_message_[30], crc); + + return (crc == this->rx_message_[30]); +} + +uint32_t GCJA5Component::get_32_bit_uint_(uint8_t start_index) { + return (((uint32_t) this->rx_message_[start_index + 3]) << 24) | + (((uint32_t) this->rx_message_[start_index + 2]) << 16) | + (((uint32_t) this->rx_message_[start_index + 1]) << 8) | ((uint32_t) this->rx_message_[start_index]); +} + +uint16_t GCJA5Component::get_16_bit_uint_(uint8_t start_index) { + return (((uint32_t) this->rx_message_[start_index + 1]) << 8) | ((uint32_t) this->rx_message_[start_index]); +} + +void GCJA5Component::parse_data_() { + ESP_LOGVV(TAG, "GCJA5 Data: "); + for (uint8_t i = 0; i < 32; i++) { + ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->rx_message_[i]), + this->rx_message_[i]); + } + + if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) { + ESP_LOGVV(TAG, "Discarding bad packet - failed checks."); + return; + } else + ESP_LOGVV(TAG, "Good packet found."); + + this->have_good_data_ = true; + uint8_t status = this->rx_message_[29]; + if (!this->first_status_log_) { + this->first_status_log_ = true; + + ESP_LOGI(TAG, "GCJA5 Status"); + ESP_LOGI(TAG, "Overall Status : %i", (status >> 6) & 0x03); + ESP_LOGI(TAG, "PD Status : %i", (status >> 4) & 0x03); + ESP_LOGI(TAG, "LD Status : %i", (status >> 2) & 0x03); + ESP_LOGI(TAG, "Fan Status : %i", (status >> 0) & 0x03); + } +} + +void GCJA5Component::dump_config() { ; } + +} // namespace gcja5 +} // namespace esphome diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h new file mode 100644 index 000000000000..7593c9032342 --- /dev/null +++ b/esphome/components/gcja5/gcja5.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace gcja5 { + +class GCJA5Component : public Component, public uart::UARTDevice { + public: + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + + void set_pmc_0_3_sensor(sensor::Sensor *pmc_0_3) { pmc_0_3_sensor_ = pmc_0_3; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_5_0_sensor(sensor::Sensor *pmc_5_0) { pmc_5_0_sensor_ = pmc_5_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + protected: + void parse_data_(); + bool calculate_checksum_(); + + uint32_t get_32_bit_uint_(uint8_t start_index); + uint16_t get_16_bit_uint_(uint8_t start_index); + uint32_t last_transmission_{0}; + std::vector rx_message_; + + bool have_good_data_{false}; + bool first_status_log_{false}; + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + sensor::Sensor *pmc_0_3_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_5_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; +}; + +} // namespace gcja5 +} // namespace esphome diff --git a/esphome/components/gcja5/sensor.py b/esphome/components/gcja5/sensor.py new file mode 100644 index 000000000000..5bcdc572ff5a --- /dev/null +++ b/esphome/components/gcja5/sensor.py @@ -0,0 +1,118 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, sensor +from esphome.const import ( + CONF_ID, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_PMC_0_5, + CONF_PMC_1_0, + CONF_PMC_2_5, + CONF_PMC_10_0, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + ICON_COUNTER, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, +) + +CODEOWNERS = ["@gcormier"] +DEPENDENCIES = ["uart"] + +gcja5_ns = cg.esphome_ns.namespace("gcja5") + +GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.PollingComponent, uart.UARTDevice) + +CONF_PMC_0_3 = "pmc_0_3" +CONF_PMC_5_0 = "pmc_5_0" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GCJA5Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend(uart.UART_DEVICE_SCHEMA) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "gcja5", baud_rate=9600, require_rx=True, parity="EVEN" +) +TYPES = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_PMC_0_3: "set_pmc_0_3_sensor", + CONF_PMC_0_5: "set_pmc_0_5_sensor", + CONF_PMC_1_0: "set_pmc_1_0_sensor", + CONF_PMC_2_5: "set_pmc_2_5_sensor", + CONF_PMC_5_0: "set_pmc_5_0_sensor", + CONF_PMC_10_0: "set_pmc_10_0_sensor", +} + + +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) + + for key, funcName in TYPES.items(): + if key in config: + sens = await sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 97a7ba3d543f..8defa4ac24e2 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -15,8 +15,14 @@ globals_ns = cg.esphome_ns.namespace("globals") GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component) RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component) +RestoringGlobalStringComponent = globals_ns.class_( + "RestoringGlobalStringComponent", cg.Component +) GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action) +CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length" + + MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( { @@ -24,6 +30,7 @@ cv.Required(CONF_TYPE): cv.string_strict, cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254), } ).extend(cv.COMPONENT_SCHEMA) @@ -32,12 +39,19 @@ @coroutine_with_priority(-100.0) async def to_code(config): type_ = cg.RawExpression(config[CONF_TYPE]) - template_args = cg.TemplateArguments(type_) restore = config[CONF_RESTORE_VALUE] - type = RestoringGlobalsComponent if restore else GlobalsComponent - res_type = type.template(template_args) + # Special casing the strings to their own class with a different save/restore mechanism + if str(type_) == "std::string" and restore: + template_args = cg.TemplateArguments( + type_, config.get(CONF_MAX_RESTORE_DATA_LENGTH, 63) + 1 + ) + type = RestoringGlobalStringComponent + else: + template_args = cg.TemplateArguments(type_) + type = RestoringGlobalsComponent if restore else GlobalsComponent + res_type = type.template(template_args) initial_value = None if CONF_INITIAL_VALUE in config: initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE]) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 101adeb31132..78808436afcd 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -65,6 +65,64 @@ template class RestoringGlobalsComponent : public Component { ESPPreferenceObject rtc_; }; +// Use with string or subclasses of strings +template class RestoringGlobalStringComponent : public Component { + public: + using value_type = T; + explicit RestoringGlobalStringComponent() = default; + explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; } + explicit RestoringGlobalStringComponent( + std::array::type, std::extent::value> initial_value) { + memcpy(this->value_, initial_value.data(), sizeof(T)); + } + + T &value() { return this->value_; } + + void setup() override { + char temp[SZ]; + this->rtc_ = global_preferences->make_preference(1944399030U ^ this->name_hash_); + bool hasdata = this->rtc_.load(&temp); + if (hasdata) { + this->value_.assign(temp + 1, temp[0]); + } + this->prev_value_.assign(this->value_); + } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void loop() override { store_value_(); } + + void on_shutdown() override { store_value_(); } + + void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } + + protected: + void store_value_() { + int diff = this->value_.compare(this->prev_value_); + if (diff != 0) { + // Make it into a length prefixed thing + unsigned char temp[SZ]; + + // If string is bigger than the allocation, do not save it. + // We don't need to waste ram setting prev_value either. + int size = this->value_.size(); + // Less than, not less than or equal, SZ includes the length byte. + if (size < SZ) { + memcpy(temp + 1, this->value_.c_str(), size); + // SZ should be pre checked at the schema level, it can't go past the char range. + temp[0] = ((unsigned char) size); + this->rtc_.save(&temp); + this->prev_value_.assign(this->value_); + } + } + } + + T value_{}; + T prev_value_{}; + uint32_t name_hash_{}; + ESPPreferenceObject rtc_; +}; + template class GlobalVarSetAction : public Action { public: explicit GlobalVarSetAction(C *parent) : parent_(parent) {} @@ -81,6 +139,7 @@ template class GlobalVarSetAction : public Action T &id(GlobalsComponent *value) { return value->value(); } template T &id(RestoringGlobalsComponent *value) { return value->value(); } +template T &id(RestoringGlobalStringComponent *value) { return value->value(); } } // namespace globals } // namespace esphome diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py new file mode 100644 index 000000000000..a7a2b46f5890 --- /dev/null +++ b/esphome/components/gp8403/__init__.py @@ -0,0 +1,40 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_VOLTAGE + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +gp8403_ns = cg.esphome_ns.namespace("gp8403") +GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) + +GP8403Voltage = gp8403_ns.enum("GP8403Voltage") + +CONF_GP8403_ID = "gp8403_id" + +VOLTAGES = { + "5V": GP8403Voltage.GP8403_VOLTAGE_5V, + "10V": GP8403Voltage.GP8403_VOLTAGE_10V, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GP8403), + cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x58)) +) + + +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_voltage(config[CONF_VOLTAGE])) diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp new file mode 100644 index 000000000000..7a08a18a8ffd --- /dev/null +++ b/esphome/components/gp8403/gp8403.cpp @@ -0,0 +1,21 @@ +#include "gp8403.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace gp8403 { + +static const char *const TAG = "gp8403"; + +static const uint8_t RANGE_REGISTER = 0x01; + +void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } + +void GP8403::dump_config() { + ESP_LOGCONFIG(TAG, "GP8403:"); + ESP_LOGCONFIG(TAG, " Voltage: %dV", this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); + LOG_I2C_DEVICE(this); +} + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h new file mode 100644 index 000000000000..65182ef30131 --- /dev/null +++ b/esphome/components/gp8403/gp8403.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace gp8403 { + +enum GP8403Voltage { + GP8403_VOLTAGE_5V = 0x00, + GP8403_VOLTAGE_10V = 0x11, +}; + +class GP8403 : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } + + protected: + GP8403Voltage voltage_; +}; + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py new file mode 100644 index 000000000000..1cf95ac6e505 --- /dev/null +++ b/esphome/components/gp8403/output/__init__.py @@ -0,0 +1,31 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import i2c, output +from esphome.const import CONF_ID, CONF_CHANNEL + +from .. import gp8403_ns, GP8403, CONF_GP8403_ID + +DEPENDENCIES = ["gp8403"] + +GP8403Output = gp8403_ns.class_( + "GP8403Output", cg.Component, i2c.I2CDevice, output.FloatOutput +) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GP8403Output), + cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), + cv.Required(CONF_CHANNEL): cv.one_of(0, 1), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + await cg.register_parented(var, config[CONF_GP8403_ID]) + + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp new file mode 100644 index 000000000000..ff73bb462785 --- /dev/null +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -0,0 +1,26 @@ +#include "gp8403_output.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace gp8403 { + +static const char *const TAG = "gp8403.output"; + +static const uint8_t OUTPUT_REGISTER = 0x02; + +void GP8403Output::dump_config() { + ESP_LOGCONFIG(TAG, "GP8403 Output:"); + ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); +} + +void GP8403Output::write_state(float state) { + uint16_t value = ((uint16_t) (state * 4095)) << 4; + i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2); + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error writing to GP8403, code %d", err); + } +} + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h new file mode 100644 index 000000000000..71e5efb1cb51 --- /dev/null +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +#include "esphome/components/gp8403/gp8403.h" + +namespace esphome { +namespace gp8403 { + +class GP8403Output : public Component, public output::FloatOutput, public Parented { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA - 1; } + + void set_channel(uint8_t channel) { this->channel_ = channel; } + + void write_state(float state) override; + + protected: + uint8_t channel_; +}; + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index e46f24ba8e35..0f1b989f77a3 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { if (tiny_gps.date.year() < 2019) return; - time::ESPTime val{}; + ESPTime val{}; val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index daff89e0a6fc..294e16dbb199 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -1,5 +1,5 @@ #include "graph.h" -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -56,7 +56,7 @@ void GraphTrace::init(Graph *g) { this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width()); } -void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { +void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) { /// Plot border if (this->border_) { buff->horizontal_line(x_offset, y_offset, this->width_, color); @@ -122,11 +122,18 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo } // Adjust limits to nice y_per_div boundaries - int yn = int(ymin / y_per_div); - int ym = int(ymax / y_per_div) + int(1 * (fmodf(ymax, y_per_div) != 0)); - ymin = yn * y_per_div; - ymax = ym * y_per_div; - yrange = ymax - ymin; + int yn = 0; + int ym = 1; + if (!std::isnan(ymin) && !std::isnan(ymax)) { + yn = (int) floorf(ymin / y_per_div); + ym = (int) ceilf(ymax / y_per_div); + if (yn == ym) { + ym++; + } + ymin = yn * y_per_div; + ymax = ym * y_per_div; + yrange = ymax - ymin; + } /// Draw grid if (!std::isnan(this->gridspacing_y_)) { @@ -296,7 +303,7 @@ void GraphLegend::init(Graph *g) { } } -void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) { +void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) { if (!legend_) return; diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h index 69c1167f54af..339a6f6d94e0 100644 --- a/esphome/components/graph/graph.h +++ b/esphome/components/graph/graph.h @@ -8,10 +8,10 @@ namespace esphome { -// forward declare DisplayBuffer +// forward declare Display namespace display { -class DisplayBuffer; -class Font; +class Display; +class BaseFont; } // namespace display namespace graph { @@ -45,8 +45,8 @@ enum ValuePositionType { class GraphLegend { public: void init(Graph *g); - void set_name_font(display::Font *font) { this->font_label_ = font; } - void set_value_font(display::Font *font) { this->font_value_ = font; } + void set_name_font(display::BaseFont *font) { this->font_label_ = font; } + void set_value_font(display::BaseFont *font) { this->font_value_ = font; } void set_width(uint32_t width) { this->width_ = width; } void set_height(uint32_t height) { this->height_ = height; } void set_border(bool val) { this->border_ = val; } @@ -63,8 +63,8 @@ class GraphLegend { ValuePositionType values_{VALUE_POSITION_TYPE_AUTO}; bool units_{true}; DirectionType direction_{DIRECTION_TYPE_AUTO}; - display::Font *font_label_{nullptr}; - display::Font *font_value_{nullptr}; + display::BaseFont *font_label_{nullptr}; + display::BaseFont *font_value_{nullptr}; // Calculated values Graph *parent_{nullptr}; // (x0) (xs,ys) (xs,ys) @@ -133,8 +133,8 @@ class GraphTrace { class Graph : public Component { public: - void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color); - void draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color); + void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color); + void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color); void setup() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } diff --git a/esphome/components/gree/__init__.py b/esphome/components/gree/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py new file mode 100644 index 000000000000..02ce7b12d486 --- /dev/null +++ b/esphome/components/gree/climate.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID, CONF_MODEL + +CODEOWNERS = ["@orestismers"] + +AUTO_LOAD = ["climate_ir"] + +gree_ns = cg.esphome_ns.namespace("gree") +GreeClimate = gree_ns.class_("GreeClimate", climate_ir.ClimateIR) + +Model = gree_ns.enum("Model") +MODELS = { + "generic": Model.GREE_GENERIC, + "yan": Model.GREE_YAN, + "yaa": Model.GREE_YAA, + "yac": Model.GREE_YAC, +} + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GreeClimate), + cv.Required(CONF_MODEL): cv.enum(MODELS), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_model(config[CONF_MODEL])) + + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp new file mode 100644 index 000000000000..1bbb443fce27 --- /dev/null +++ b/esphome/components/gree/gree.cpp @@ -0,0 +1,157 @@ +#include "gree.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace gree { + +static const char *const TAG = "gree.climate"; + +void GreeClimate::set_model(Model model) { this->model_ = model; } + +void GreeClimate::transmit_state() { + uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00}; + + remote_state[0] = this->fan_speed_() | this->operation_mode_(); + remote_state[1] = this->temperature_(); + + if (this->model_ == GREE_YAN) { + remote_state[2] = 0x60; + remote_state[3] = 0x50; + remote_state[4] = this->vertical_swing_(); + } + + if (this->model_ == GREE_YAC) { + remote_state[4] |= (this->horizontal_swing_() << 4); + } + + if (this->model_ == GREE_YAA || this->model_ == GREE_YAC) { + remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO,LIGHT,HEALTH,X-FAN + remote_state[3] = 0x50; // bits 4..7 always 0101 + remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010 + + if (this->vertical_swing_() == GREE_VDIR_SWING) { + remote_state[0] |= (1 << 6); // Enable swing by setting bit 6 + } else if (this->vertical_swing_() != GREE_VDIR_AUTO) { + remote_state[5] = this->vertical_swing_(); + } + } + + // Calculate the checksum + if (this->model_ == GREE_YAN) { + remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); + } else { + remote_state[7] = + ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) & + 0x0F) + << 4) | + (remote_state[7] & 0x0F); + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(GREE_IR_FREQUENCY); + + data->mark(GREE_HEADER_MARK); + data->space(GREE_HEADER_SPACE); + + for (int i = 0; i < 4; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(GREE_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE); + } + } + + data->mark(GREE_BIT_MARK); + data->space(GREE_ZERO_SPACE); + data->mark(GREE_BIT_MARK); + data->space(GREE_ONE_SPACE); + data->mark(GREE_BIT_MARK); + data->space(GREE_ZERO_SPACE); + + data->mark(GREE_BIT_MARK); + data->space(GREE_MESSAGE_SPACE); + + for (int i = 4; i < 8; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(GREE_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE); + } + } + + data->mark(GREE_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t GreeClimate::operation_mode_() { + uint8_t operating_mode = GREE_MODE_ON; + + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= GREE_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= GREE_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= GREE_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= GREE_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= GREE_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = GREE_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t GreeClimate::fan_speed_() { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + return GREE_FAN_1; + case climate::CLIMATE_FAN_MEDIUM: + return GREE_FAN_2; + case climate::CLIMATE_FAN_HIGH: + return GREE_FAN_3; + case climate::CLIMATE_FAN_AUTO: + default: + return GREE_FAN_AUTO; + } +} + +uint8_t GreeClimate::horizontal_swing_() { + switch (this->swing_mode) { + case climate::CLIMATE_SWING_HORIZONTAL: + case climate::CLIMATE_SWING_BOTH: + return GREE_HDIR_SWING; + default: + return GREE_HDIR_MANUAL; + } +} + +uint8_t GreeClimate::vertical_swing_() { + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + case climate::CLIMATE_SWING_BOTH: + return GREE_VDIR_SWING; + default: + return GREE_VDIR_MANUAL; + } +} + +uint8_t GreeClimate::temperature_() { + return (uint8_t) roundf(clamp(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX)); +} + +} // namespace gree +} // namespace esphome diff --git a/esphome/components/gree/gree.h b/esphome/components/gree/gree.h new file mode 100644 index 000000000000..e7131a2b896e --- /dev/null +++ b/esphome/components/gree/gree.h @@ -0,0 +1,97 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace gree { + +// Values for GREE IR Controllers +// Temperature +const uint8_t GREE_TEMP_MIN = 16; // Celsius +const uint8_t GREE_TEMP_MAX = 30; // Celsius + +// Modes +const uint8_t GREE_MODE_AUTO = 0x00; +const uint8_t GREE_MODE_COOL = 0x01; +const uint8_t GREE_MODE_HEAT = 0x04; +const uint8_t GREE_MODE_DRY = 0x02; +const uint8_t GREE_MODE_FAN = 0x03; + +const uint8_t GREE_MODE_OFF = 0x00; +const uint8_t GREE_MODE_ON = 0x08; + +// Fan Speed +const uint8_t GREE_FAN_AUTO = 0x00; +const uint8_t GREE_FAN_1 = 0x10; +const uint8_t GREE_FAN_2 = 0x20; +const uint8_t GREE_FAN_3 = 0x30; +const uint8_t GREE_FAN_TURBO = 0x80; + +// IR Transmission +const uint32_t GREE_IR_FREQUENCY = 38000; +const uint32_t GREE_HEADER_MARK = 9000; +const uint32_t GREE_HEADER_SPACE = 4000; +const uint32_t GREE_BIT_MARK = 620; +const uint32_t GREE_ONE_SPACE = 1600; +const uint32_t GREE_ZERO_SPACE = 540; +const uint32_t GREE_MESSAGE_SPACE = 19000; + +// Timing specific for YAC features (I-Feel mode) +const uint32_t GREE_YAC_HEADER_MARK = 6000; +const uint32_t GREE_YAC_HEADER_SPACE = 3000; +const uint32_t GREE_YAC_BIT_MARK = 650; + +// State Frame size +const uint8_t GREE_STATE_FRAME_SIZE = 8; + +// Only available on YAN +// Vertical air directions. Note that these cannot be set on all heat pumps +const uint8_t GREE_VDIR_AUTO = 0x00; +const uint8_t GREE_VDIR_MANUAL = 0x00; +const uint8_t GREE_VDIR_SWING = 0x01; +const uint8_t GREE_VDIR_UP = 0x02; +const uint8_t GREE_VDIR_MUP = 0x03; +const uint8_t GREE_VDIR_MIDDLE = 0x04; +const uint8_t GREE_VDIR_MDOWN = 0x05; +const uint8_t GREE_VDIR_DOWN = 0x06; + +// Only available on YAC +// Horizontal air directions. Note that these cannot be set on all heat pumps +const uint8_t GREE_HDIR_AUTO = 0x00; +const uint8_t GREE_HDIR_MANUAL = 0x00; +const uint8_t GREE_HDIR_SWING = 0x01; +const uint8_t GREE_HDIR_LEFT = 0x02; +const uint8_t GREE_HDIR_MLEFT = 0x03; +const uint8_t GREE_HDIR_MIDDLE = 0x04; +const uint8_t GREE_HDIR_MRIGHT = 0x05; +const uint8_t GREE_HDIR_RIGHT = 0x06; + +// Model codes +enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC }; + +class GreeClimate : public climate_ir::ClimateIR { + public: + GreeClimate() + : climate_ir::ClimateIR(GREE_TEMP_MIN, GREE_TEMP_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_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + void set_model(Model model); + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + + uint8_t operation_mode_(); + uint8_t fan_speed_(); + uint8_t horizontal_swing_(); + uint8_t vertical_swing_(); + uint8_t temperature_(); + + Model model_{}; +}; + +} // namespace gree +} // namespace esphome diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py new file mode 100644 index 000000000000..7db0198a89e0 --- /dev/null +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -0,0 +1,177 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c + +from esphome.const import ( + CONF_ID, + CONF_CHANNEL, + CONF_SPEED, + CONF_DIRECTION, + CONF_ADDRESS, +) + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@max246"] + +MULTI_CONF = True + +grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng") +GROVE_TB6612FNG = grove_tb6612fng_ns.class_( + "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice +) +GROVETB6612FNGMotorRunAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorRunAction", automation.Action +) +GROVETB6612FNGMotorBrakeAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorBrakeAction", automation.Action +) +GROVETB6612FNGMotorStopAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorStopAction", automation.Action +) +GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorStandbyAction", automation.Action +) +GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorNoStandbyAction", automation.Action +) +GROVETB6612FNGMotorChangeAddressAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorChangeAddressAction", automation.Action +) + +DIRECTION_TYPE = { + "FORWARD": 1, + "BACKWARD": 2, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(GROVE_TB6612FNG), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x14)) +) + + +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) + + +@automation.register_action( + "grove_tb6612fng.run", + GROVETB6612FNGMotorRunAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + cv.Required(CONF_SPEED): cv.templatable(cv.int_range(min=0, max=255)), + cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), + } + ), +) +async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) + template_speed = ( + template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed + ) + cg.add(var.set_channel(template_channel)) + cg.add(var.set_speed(template_speed)) + return var + + +@automation.register_action( + "grove_tb6612fng.break", + GROVETB6612FNGMotorBrakeAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_tb6612fng.stop", + GROVETB6612FNGMotorStopAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_tb6612fng.standby", + GROVETB6612FNGMotorStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var + + +@automation.register_action( + "grove_tb6612fng.no_standby", + GROVETB6612FNGMotorNoStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var + + +@automation.register_action( + "grove_tb6612fng.change_address", + GROVETB6612FNGMotorChangeAddressAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_ADDRESS): cv.i2c_address, + } + ), +) +async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_ADDRESS], args, int) + cg.add(var.set_address(template_channel)) + return var diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp new file mode 100644 index 000000000000..621b7968a4b9 --- /dev/null +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -0,0 +1,171 @@ +#include "grove_tb6612fng.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace grove_tb6612fng { + +static const char *const TAG = "GroveMotorDriveTB6612FNG"; + +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE = 0x00; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STOP = 0x01; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CW = 0x02; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CCW = 0x03; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY = 0x04; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY = 0x05; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN = 0x06; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP = 0x07; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN = 0x08; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR = 0x11; + +void GroveMotorDriveTB6612FNG::dump_config() { + ESP_LOGCONFIG(TAG, "GroveMotorDriveTB6612FNG:"); + LOG_I2C_DEVICE(this); +} + +void GroveMotorDriveTB6612FNG::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grove Motor Drive TB6612FNG ..."); + if (!this->standby()) { + this->mark_failed(); + return; + } +} + +bool GroveMotorDriveTB6612FNG::standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +bool GroveMotorDriveTB6612FNG::not_standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set not standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) { + if (addr == 0x00 || addr >= 0x80) { + return; + } + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR, &addr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set new i2c address failed!"); + this->status_set_warning(); + return; + } + this->set_i2c_address(addr); +} + +void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) { + speed = clamp(speed, -255, 255); + + buffer_[0] = channel; + if (speed >= 0) { + buffer_[1] = speed; + } else { + buffer_[1] = (uint8_t) (-speed); + } + + if (speed >= 0) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } else { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_brake(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Break motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_stop(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STOP, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Stop dc motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm) { + uint8_t cw = 0; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + if (steps > 0) { + cw = 1; + } + // stop + else if (steps == 0) { + this->stepper_stop(); + return; + } else if (steps == INT16_MIN) { + steps = INT16_MAX; + } else { + steps = -steps; + } + + rpm = clamp(rpm, 1, 300); + + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = steps; + buffer_[3] = (steps >> 8); + buffer_[4] = ms_per_step; + buffer_[5] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_stop() { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Send stop stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw) { + // 4=>infinite ccw 5=>infinite cw + uint8_t cw = (is_cw) ? 5 : 4; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + rpm = clamp(rpm, 1, 300); + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = ms_per_step; + buffer_[3] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN, buffer_, 4) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Write stepper keep run failed"); + this->status_set_warning(); + return; + } +} +} // namespace grove_tb6612fng +} // namespace esphome diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h new file mode 100644 index 000000000000..2743ef4ed7a0 --- /dev/null +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -0,0 +1,215 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" +//#include "esphome/core/helpers.h" + +/* + Grove_Motor_Driver_TB6612FNG.h + A library for the Grove - Motor Driver(TB6612FNG) + Copyright (c) 2018 seeed technology co., ltd. + Website : www.seeed.cc + Author : Jerry Yip + Create Time: 2018-06 + Version : 0.1 + Change Log : + The MIT License (MIT) + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +namespace esphome { +namespace grove_tb6612fng { + +enum MotorChannelTypeT { + MOTOR_CHA = 0, + MOTOR_CHB = 1, +}; + +enum StepperModeTypeT { + FULL_STEP = 0, + WAVE_DRIVE = 1, + HALF_STEP = 2, + MICRO_STEPPING = 3, +}; + +class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + /************************************************************* + Description + Enter standby mode. Normally you don't need to call this, except that + you have called notStandby() before. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool standby(); + + /************************************************************* + Description + Exit standby mode. Motor driver does't do any action at this mode. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool not_standby(); + + /************************************************************* + Description + Set an new I2C address. + Parameter + addr: 0x01~0x7f + Return + Null. + *************************************************************/ + void set_i2c_addr(uint8_t addr); + + /***********************************change_address + Drive a motor. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + speed: -255~255, if speed > 0, motor moves clockwise. + Note that there is always a starting speed(a starting voltage) for motor. + If the input voltage is 5V, the starting speed should larger than 100 or + smaller than -100. + Return + Null. + *************************************************************/ + void dc_motor_run(uint8_t channel, int16_t speed); + + /************************************************************* + Description + Brake, stop the motor immediately + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_brake(uint8_t channel); + + /************************************************************* + Description + Stop the motor slowly. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_stop(uint8_t channel); + + /************************************************************* + Description + Drive a stepper. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + steps: The number of steps to run, range from -32768 to 32767. + When steps = 0, the stepper stops. + When steps > 0, the stepper runs clockwise. When steps < 0, the stepper runs anticlockwise. + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + Return + Null. + *************************************************************/ + void stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm); + + /************************************************************* + Description + Stop a stepper. + Parameter + Null. + Return + Null. + *************************************************************/ + void stepper_stop(); + + // keeps moving(direction same as the last move, default to clockwise) + /************************************************************* + Description + Keep a stepper running. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + is_cw: Set the running direction, true for clockwise and false for anti-clockwise. + Return + Null. + *************************************************************/ + void stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw); + + private: + uint8_t buffer_[16]; +}; + +template +class GROVETB6612FNGMotorRunAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint16_t, speed) + + void play(Ts... x) override { + auto channel = this->channel_.value(x...); + auto speed = this->speed_.value(x...); + this->parent_->dc_motor_run(channel, speed); + } +}; + +template +class GROVETB6612FNGMotorBrakeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStopAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->standby(); } +}; + +template +class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->not_standby(); } +}; + +template +class GROVETB6612FNGMotorChangeAddressAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, address) + + void play(Ts... x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } +}; + +} // namespace grove_tb6612fng +} // namespace esphome diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index ed753c4d3fb2..c4ed5ab841ca 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -9,11 +9,42 @@ 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, 95}; // indexed with enum GrowattProtocolVersion +void GrowattSolar::loop() { + // If update() was unable to send we retry until we can send. + if (!this->waiting_to_update_) + return; + update(); +} + void GrowattSolar::update() { + // If our last send has had no reply yet, and it wasn't that long ago, do nothing. + uint32_t now = millis(); + if (now - this->last_send_ < this->get_update_interval() / 2) { + return; + } + + // The bus might be slow, or there might be other devices, or other components might be talking to our device. + if (this->waiting_for_response()) { + this->waiting_to_update_ = true; + return; + } + + this->waiting_to_update_ = false; this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); + this->last_send_ = millis(); } void GrowattSolar::on_modbus_data(const std::vector &data) { + // Other components might be sending commands to our device. But we don't get called with enough + // context to know what is what. So if we didn't do a send, we ignore the data. + if (!this->last_send_) + return; + this->last_send_ = 0; + + // Also ignore the data if the message is too short. Otherwise we will publish invalid values. + if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2) + return; + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { if (sensor == nullptr) return; diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index d1b08b534a1a..b0ddd4b99d8b 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -19,6 +19,7 @@ enum GrowattProtocolVersion { class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { public: + void loop() override; void update() override; void on_modbus_data(const std::vector &data) override; void dump_config() override; @@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: + bool waiting_to_update_; + uint32_t last_send_; + struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index f95d679c3ea5..0db15ae53e56 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -6,6 +6,9 @@ CONF_CURRENT, CONF_FREQUENCY, CONF_ID, + CONF_PHASE_A, + CONF_PHASE_B, + CONF_PHASE_C, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -21,10 +24,6 @@ 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" diff --git a/esphome/components/haier/__init__.py b/esphome/components/haier/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h new file mode 100644 index 000000000000..84e4554db879 --- /dev/null +++ b/esphome/components/haier/automation.h @@ -0,0 +1,130 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "haier_base.h" +#include "hon_climate.h" + +namespace esphome { +namespace haier { + +template class DisplayOnAction : public Action { + public: + DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class DisplayOffAction : public Action { + public: + DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class BeeperOnAction : public Action { + public: + BeeperOnAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(true); } + + protected: + HonClimate *parent_; +}; + +template class BeeperOffAction : public Action { + public: + BeeperOffAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(false); } + + protected: + HonClimate *parent_; +}; + +template class VerticalAirflowAction : public Action { + public: + VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowVerticalDirection, direction) + void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HorizontalAirflowAction : public Action { + public: + HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction) + void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HealthOnAction : public Action { + public: + HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class HealthOffAction : public Action { + public: + HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class StartSelfCleaningAction : public Action { + public: + StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_self_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class StartSteriCleaningAction : public Action { + public: + StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_steri_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class PowerOnAction : public Action { + public: + PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_on_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerOffAction : public Action { + public: + PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_off_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerToggleAction : public Action { + public: + PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->toggle_power(); } + + protected: + HaierClimateBase *parent_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py new file mode 100644 index 000000000000..d796f13581bc --- /dev/null +++ b/esphome/components/haier/climate.py @@ -0,0 +1,427 @@ +import logging +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.components import uart, sensor, climate, logger +from esphome import automation +from esphome.const import ( + CONF_BEEPER, + CONF_ID, + CONF_LEVEL, + CONF_LOGGER, + CONF_LOGS, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_PROTOCOL, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_TARGET_TEMPERATURE, + CONF_TEMPERATURE_STEP, + CONF_VISUAL, + CONF_WIFI, + DEVICE_CLASS_TEMPERATURE, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, + CONF_CURRENT_TEMPERATURE, +) + +_LOGGER = logging.getLogger(__name__) + +PROTOCOL_MIN_TEMPERATURE = 16.0 +PROTOCOL_MAX_TEMPERATURE = 30.0 +PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 +PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 + +CODEOWNERS = ["@paveldn"] +AUTO_LOAD = ["sensor"] +DEPENDENCIES = ["climate", "uart"] +CONF_WIFI_SIGNAL = "wifi_signal" +CONF_ANSWER_TIMEOUT = "answer_timeout" +CONF_DISPLAY = "display" +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" + +PROTOCOL_HON = "HON" +PROTOCOL_SMARTAIR2 = "SMARTAIR2" +PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2] + +haier_ns = cg.esphome_ns.namespace("haier") +HaierClimateBase = haier_ns.class_( + "HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component +) +HonClimate = haier_ns.class_("HonClimate", HaierClimateBase) +Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase) + + +AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection", True) +AIRFLOW_VERTICAL_DIRECTION_OPTIONS = { + "HEALTH_UP": AirflowVerticalDirection.HEALTH_UP, + "MAX_UP": AirflowVerticalDirection.MAX_UP, + "UP": AirflowVerticalDirection.UP, + "CENTER": AirflowVerticalDirection.CENTER, + "DOWN": AirflowVerticalDirection.DOWN, + "HEALTH_DOWN": AirflowVerticalDirection.HEALTH_DOWN, +} + +AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection", True) +AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = { + "MAX_LEFT": AirflowHorizontalDirection.MAX_LEFT, + "LEFT": AirflowHorizontalDirection.LEFT, + "CENTER": AirflowHorizontalDirection.CENTER, + "RIGHT": AirflowHorizontalDirection.RIGHT, + "MAX_RIGHT": AirflowHorizontalDirection.MAX_RIGHT, +} + +SUPPORTED_SWING_MODES_OPTIONS = { + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, +} + +SUPPORTED_CLIMATE_MODES_OPTIONS = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, # always available + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, # always available + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, +} + +SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + + +def validate_visual(config): + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + if CONF_MIN_TEMPERATURE in visual_config: + min_temp = visual_config[CONF_MIN_TEMPERATURE] + if min_temp < PROTOCOL_MIN_TEMPERATURE: + raise cv.Invalid( + f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE + if CONF_MAX_TEMPERATURE in visual_config: + max_temp = visual_config[CONF_MAX_TEMPERATURE] + if max_temp > PROTOCOL_MAX_TEMPERATURE: + raise cv.Invalid( + f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE + if CONF_TEMPERATURE_STEP in visual_config: + temp_step = config[CONF_VISUAL][CONF_TEMPERATURE_STEP][ + CONF_TARGET_TEMPERATURE + ] + if ((int)(temp_step * 2)) / 2 != temp_step: + raise cv.Invalid( + f"Configured visual temperature step {temp_step} is wrong, it should be a multiple of 0.5" + ) + else: + config[CONF_VISUAL][CONF_TEMPERATURE_STEP] = { + CONF_TARGET_TEMPERATURE: PROTOCOL_TARGET_TEMPERATURE_STEP, + CONF_CURRENT_TEMPERATURE: PROTOCOL_CURRENT_TEMPERATURE_STEP, + } + else: + config[CONF_VISUAL] = { + CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE, + CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE, + CONF_TEMPERATURE_STEP: { + CONF_TARGET_TEMPERATURE: PROTOCOL_TARGET_TEMPERATURE_STEP, + CONF_CURRENT_TEMPERATURE: PROTOCOL_CURRENT_TEMPERATURE_STEP, + }, + } + return config + + +BASE_CONFIG_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) + ), + cv.Optional( + CONF_SUPPORTED_SWING_MODES, + default=[ + "OFF", + "VERTICAL", + "HORIZONTAL", + "BOTH", + ], + ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, + cv.Optional(CONF_DISPLAY): cv.boolean, + cv.Optional( + CONF_ANSWER_TIMEOUT, + ): cv.positive_time_period_milliseconds, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Smartair2Climate), + cv.Optional( + CONF_SUPPORTED_PRESETS, + default=list( + SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys() + ), + ): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True) + ), + } + ), + PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HonClimate), + cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional( + CONF_SUPPORTED_PRESETS, + default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), + ): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) + ), + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ), + }, + key=CONF_PROTOCOL, + default_type=PROTOCOL_SMARTAIR2, + upper=True, + ), + validate_visual, +) + + +# Actions +DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action) +DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action) +BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action) +StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action) +StartSteriCleaningAction = haier_ns.class_( + "StartSteriCleaningAction", automation.Action +) +VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action) +HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action) +HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action) +HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action) +PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action) + +HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HaierClimateBase), + } +) + +HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HonClimate), + } +) + + +@automation.register_action( + "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def display_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA +) +async def beeper_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Start self cleaning or steri-cleaning action action +@automation.register_action( + "climate.haier.start_self_cleaning", + StartSelfCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +@automation.register_action( + "climate.haier.start_steri_cleaning", + StartSteriCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +async def start_cleaning_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Set vertical airflow direction action +@automation.register_action( + "climate.haier.set_vertical_airflow", + VerticalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +# Set horizontal airflow direction action +@automation.register_action( + "climate.haier.set_horizontal_airflow", + HorizontalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def health_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA +) +async def power_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +def _final_validate(config): + full_config = fv.full_config.get() + if CONF_LOGGER in full_config: + _level = "NONE" + logger_config = full_config[CONF_LOGGER] + if CONF_LOGS in logger_config: + if "haier.protocol" in logger_config[CONF_LOGS]: + _level = logger_config[CONF_LOGS]["haier.protocol"] + else: + _level = logger_config[CONF_LEVEL] + _LOGGER.info("Detected log level for Haier protocol: %s", _level) + if _level not in logger.LOG_LEVEL_SEVERITY: + raise cv.Invalid("Unknown log level for Haier protocol") + _severity = logger.LOG_LEVEL_SEVERITY.index(_level) + cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}") + else: + _LOGGER.info( + "No logger component found, logging for Haier protocol is disabled" + ) + cg.add_build_flag("-DHAIER_LOG_LEVEL=0") + if ( + (CONF_WIFI_SIGNAL in config) + and (config[CONF_WIFI_SIGNAL]) + and CONF_WIFI not in full_config + ): + raise cv.Invalid( + f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + cg.add(haier_ns.init_haier_protocol_logging()) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + await climate.register_climate(var, config) + + cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + if CONF_BEEPER in config: + cg.add(var.set_beeper_state(config[CONF_BEEPER])) + if CONF_DISPLAY in config: + cg.add(var.set_display_state(config[CONF_DISPLAY])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_ANSWER_TIMEOUT in config: + cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) + # https://github.com/paveldn/HaierProtocol + cg.add_library("pavlodn/HaierProtocol", "0.9.20") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp new file mode 100644 index 000000000000..22899b1a707d --- /dev/null +++ b/esphome/components/haier/haier_base.cpp @@ -0,0 +1,350 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif +#include "haier_base.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000; +constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; +constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; +constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; +constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; +constexpr size_t CONTROL_TIMEOUT_MS = 7000; +constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied + +#if (HAIER_LOG_LEVEL > 4) +// To reduce size of binary this function only available when log level is Verbose +const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { + static const char *phase_names[] = { + "SENDING_INIT_1", + "WAITING_INIT_1_ANSWER", + "SENDING_INIT_2", + "WAITING_INIT_2_ANSWER", + "SENDING_FIRST_STATUS_REQUEST", + "WAITING_FIRST_STATUS_ANSWER", + "SENDING_ALARM_STATUS_REQUEST", + "WAITING_ALARM_STATUS_ANSWER", + "IDLE", + "UNKNOWN", + "SENDING_STATUS_REQUEST", + "WAITING_STATUS_ANSWER", + "SENDING_UPDATE_SIGNAL_REQUEST", + "WAITING_UPDATE_SIGNAL_ANSWER", + "SENDING_SIGNAL_LEVEL", + "WAITING_SIGNAL_LEVEL_ANSWER", + "SENDING_CONTROL", + "WAITING_CONTROL_ANSWER", + "SENDING_POWER_ON_COMMAND", + "WAITING_POWER_ON_ANSWER", + "SENDING_POWER_OFF_COMMAND", + "WAITING_POWER_OFF_ANSWER", + "UNKNOWN" // Should be the last! + }; + int phase_index = (int) phase; + if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) + phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; + return phase_names[phase_index]; +} +#endif + +HaierClimateBase::HaierClimateBase() + : haier_protocol_(*this), + protocol_phase_(ProtocolPhases::SENDING_INIT_1), + action_request_(ActionRequest::NO_ACTION), + display_status_(true), + health_mode_(false), + force_send_control_(false), + forced_publish_(false), + forced_request_status_(false), + first_control_attempt_(false), + reset_protocol_request_(false), + send_wifi_signal_(true) { + this->traits_ = climate::ClimateTraits(); + this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_HEAT_COOL}); + this->traits_.set_supported_fan_modes( + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); + this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); + this->traits_.set_supports_current_temperature(true); +} + +HaierClimateBase::~HaierClimateBase() {} + +void HaierClimateBase::set_phase(ProtocolPhases phase) { + if (this->protocol_phase_ != phase) { +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); +#else + ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase); +#endif + this->protocol_phase_ = phase; + } +} + +bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, + std::chrono::steady_clock::time_point tpoint, size_t timeout) { + return std::chrono::duration_cast(now - tpoint).count() > timeout; +} + +bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); +} + +bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS); +} + +bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); +} + +#ifdef USE_WIFI +haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { + static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; + if (wifi::global_wifi_component->is_connected()) { + wifi_status_data[1] = 0; + int8_t rssi = wifi::global_wifi_component->wifi_rssi(); + wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f); + ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]); + } else { + ESP_LOGD(TAG, "WiFi is not connected"); + wifi_status_data[1] = 1; + wifi_status_data[3] = 0; + } + return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); +} +#endif + +bool HaierClimateBase::get_display_state() const { return this->display_status_; } + +void HaierClimateBase::set_display_state(bool state) { + if (this->display_status_ != state) { + this->display_status_ = state; + this->set_force_send_control_(true); + } +} + +bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } + +void HaierClimateBase::set_health_mode(bool state) { + if (this->health_mode_ != state) { + this->health_mode_ = state; + this->set_force_send_control_(true); + } +} + +void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } + +void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } + +void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } + +void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { + this->traits_.set_supported_swing_modes(modes); + if (!modes.empty()) + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); +} + +void HaierClimateBase::set_answer_timeout(uint32_t timeout) { + this->answer_timeout_ = std::chrono::milliseconds(timeout); +} + +void HaierClimateBase::set_supported_modes(const std::set &modes) { + this->traits_.set_supported_modes(modes); + this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available + this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available +} + +void HaierClimateBase::set_supported_presets(const std::set &presets) { + this->traits_.set_supported_presets(presets); + if (!presets.empty()) + this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); +} + +void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } + +haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, + uint8_t expected_request_message_type, + uint8_t answer_message_type, + uint8_t expected_answer_message_type, + ProtocolPhases expected_phase) { + haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; + if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) + result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) + result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) + result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + if (is_message_invalid(answer_message_type)) + result = haier_protocol::HandlerError::INVALID_ANSWER; + return result; +} + +haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); +#else + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); +#endif + if (this->protocol_phase_ > ProtocolPhases::IDLE) { + this->set_phase(ProtocolPhases::IDLE); + } else { + this->set_phase(ProtocolPhases::SENDING_INIT_1); + } + return haier_protocol::HandlerError::HANDLER_OK; +} + +void HaierClimateBase::setup() { + ESP_LOGI(TAG, "Haier initialization..."); + // Set timestamp here to give AC time to boot + this->last_request_timestamp_ = std::chrono::steady_clock::now(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + this->set_handlers(); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); +} + +void HaierClimateBase::dump_config() { + LOG_CLIMATE("", "Haier Climate", this); + ESP_LOGCONFIG(TAG, " Device communication status: %s", + (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none"); +} + +void HaierClimateBase::loop() { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + if ((std::chrono::duration_cast(now - this->last_valid_status_timestamp_).count() > + COMMUNICATION_TIMEOUT_MS) || + (this->reset_protocol_request_)) { + if (this->protocol_phase_ >= ProtocolPhases::IDLE) { + // No status too long, reseting protocol + if (this->reset_protocol_request_) { + this->reset_protocol_request_ = false; + ESP_LOGW(TAG, "Protocol reset requested"); + } else { + ESP_LOGW(TAG, "Communication timeout, reseting protocol"); + } + this->last_valid_status_timestamp_ = now; + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + return; + } else { + // No need to reset protocol if we didn't pass initialization phase + this->last_valid_status_timestamp_ = now; + } + }; + if ((this->protocol_phase_ == ProtocolPhases::IDLE) || + (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { + // If control message or action is pending we should send it ASAP unless we are in initialisation + // procedure or waiting for an answer + if (this->action_request_ != ActionRequest::NO_ACTION) { + this->process_pending_action(); + } else if (this->hvac_settings_.valid || this->force_send_control_) { + ESP_LOGV(TAG, "Control packet is pending..."); + this->set_phase(ProtocolPhases::SENDING_CONTROL); + } + } + this->process_phase(now); + this->haier_protocol_.loop(); +} + +void HaierClimateBase::process_pending_action() { + ActionRequest request = this->action_request_; + if (this->action_request_ == ActionRequest::TOGGLE_POWER) { + request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; + } + switch (request) { + case ActionRequest::TURN_POWER_ON: + this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); + break; + case ActionRequest::TURN_POWER_OFF: + this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); + break; + case ActionRequest::TOGGLE_POWER: + case ActionRequest::NO_ACTION: + // shouldn't get here, do nothing + break; + default: + ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); + break; + } + this->action_request_ = ActionRequest::NO_ACTION; +} + +ClimateTraits HaierClimateBase::traits() { return traits_; } + +void HaierClimateBase::control(const ClimateCall &call) { + ESP_LOGD("Control", "Control call"); + if (this->protocol_phase_ < ProtocolPhases::IDLE) { + ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); + return; // cancel the control, we cant do it without a poll answer. + } + if (this->hvac_settings_.valid) { + ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); + } + { + if (call.get_mode().has_value()) + this->hvac_settings_.mode = call.get_mode(); + if (call.get_fan_mode().has_value()) + this->hvac_settings_.fan_mode = call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->hvac_settings_.swing_mode = call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->hvac_settings_.target_temperature = call.get_target_temperature(); + if (call.get_preset().has_value()) + this->hvac_settings_.preset = call.get_preset(); + this->hvac_settings_.valid = true; + } + this->first_control_attempt_ = true; +} + +void HaierClimateBase::HvacSettings::reset() { + this->valid = false; + this->mode.reset(); + this->fan_mode.reset(); + this->swing_mode.reset(); + this->target_temperature.reset(); + this->preset.reset(); +} + +void HaierClimateBase::set_force_send_control_(bool status) { + this->force_send_control_ = status; + if (status) { + this->first_control_attempt_ = true; + } +} + +void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { + if (this->answer_timeout_.has_value()) { + this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value()); + } else { + this->haier_protocol_.send_message(command, use_crc); + } + this->last_request_timestamp_ = std::chrono::steady_clock::now(); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h new file mode 100644 index 000000000000..b2446d6fb5a5 --- /dev/null +++ b/esphome/components/haier/haier_base.h @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +enum class ActionRequest : uint8_t { + NO_ACTION = 0, + TURN_POWER_ON = 1, + TURN_POWER_OFF = 2, + TOGGLE_POWER = 3, + START_SELF_CLEAN = 4, // only hOn + START_STERI_CLEAN = 5, // only hOn +}; + +class HaierClimateBase : public esphome::Component, + public esphome::climate::Climate, + public esphome::uart::UARTDevice, + public haier_protocol::ProtocolStream { + public: + HaierClimateBase(); + HaierClimateBase(const HaierClimateBase &) = delete; + HaierClimateBase &operator=(const HaierClimateBase &) = delete; + ~HaierClimateBase(); + void setup() override; + void loop() override; + void control(const esphome::climate::ClimateCall &call) override; + void dump_config() override; + float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } + void set_fahrenheit(bool fahrenheit); + void set_display_state(bool state); + bool get_display_state() const; + void set_health_mode(bool state); + bool get_health_mode() const; + void send_power_on_command(); + void send_power_off_command(); + void toggle_power(); + void reset_protocol() { this->reset_protocol_request_ = true; }; + void set_supported_modes(const std::set &modes); + void set_supported_swing_modes(const std::set &modes); + void set_supported_presets(const std::set &presets); + size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; + size_t read_array(uint8_t *data, size_t len) noexcept override { + return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; + }; + void write_array(const uint8_t *data, size_t len) noexcept override { + esphome::uart::UARTDevice::write_array(data, len); + }; + bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; + void set_answer_timeout(uint32_t timeout); + void set_send_wifi(bool send_wifi); + + protected: + enum class ProtocolPhases { + UNKNOWN = -1, + // INITIALIZATION + SENDING_INIT_1 = 0, + WAITING_INIT_1_ANSWER = 1, + SENDING_INIT_2 = 2, + WAITING_INIT_2_ANSWER = 3, + SENDING_FIRST_STATUS_REQUEST = 4, + WAITING_FIRST_STATUS_ANSWER = 5, + SENDING_ALARM_STATUS_REQUEST = 6, + WAITING_ALARM_STATUS_ANSWER = 7, + // FUNCTIONAL STATE + IDLE = 8, + SENDING_STATUS_REQUEST = 10, + WAITING_STATUS_ANSWER = 11, + SENDING_UPDATE_SIGNAL_REQUEST = 12, + WAITING_UPDATE_SIGNAL_ANSWER = 13, + SENDING_SIGNAL_LEVEL = 14, + WAITING_SIGNAL_LEVEL_ANSWER = 15, + SENDING_CONTROL = 16, + WAITING_CONTROL_ANSWER = 17, + SENDING_POWER_ON_COMMAND = 18, + WAITING_POWER_ON_ANSWER = 19, + SENDING_POWER_OFF_COMMAND = 20, + WAITING_POWER_OFF_ANSWER = 21, + NUM_PROTOCOL_PHASES + }; +#if (HAIER_LOG_LEVEL > 4) + const char *phase_to_string_(ProtocolPhases phase); +#endif + virtual void set_handlers() = 0; + virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; + virtual bool is_message_invalid(uint8_t message_type) = 0; + virtual void process_pending_action(); + esphome::climate::ClimateTraits traits() override; + // Answers handlers + haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, + uint8_t answer_message_type, uint8_t expected_answer_message_type, + ProtocolPhases expected_phase); + // Timeout handler + haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); + // Helper functions + void set_force_send_control_(bool status); + void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); + virtual void set_phase(ProtocolPhases phase); + bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, + size_t timeout); + bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); +#ifdef USE_WIFI + haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); +#endif + + struct HvacSettings { + esphome::optional mode; + esphome::optional fan_mode; + esphome::optional swing_mode; + esphome::optional target_temperature; + esphome::optional preset; + bool valid; + HvacSettings() : valid(false){}; + void reset(); + }; + haier_protocol::ProtocolHandler haier_protocol_; + ProtocolPhases protocol_phase_; + ActionRequest action_request_; + uint8_t fan_mode_speed_; + uint8_t other_modes_fan_speed_; + bool display_status_; + bool health_mode_; + bool force_send_control_; + bool forced_publish_; + bool forced_request_status_; + bool first_control_attempt_; + bool reset_protocol_request_; + esphome::climate::ClimateTraits traits_; + HvacSettings hvac_settings_; + std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages + std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout + std::chrono::steady_clock::time_point last_status_request_; // To request AC status + std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message + optional answer_timeout_; // Message answer timeout + bool send_wifi_signal_; + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp new file mode 100644 index 000000000000..d4944410f703 --- /dev/null +++ b/esphome/components/haier/hon_climate.cpp @@ -0,0 +1,833 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "hon_climate.h" +#include "hon_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; + +hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { + switch (direction) { + case AirflowVerticalDirection::HEALTH_UP: + return hon_protocol::VerticalSwingMode::HEALTH_UP; + case AirflowVerticalDirection::MAX_UP: + return hon_protocol::VerticalSwingMode::MAX_UP; + case AirflowVerticalDirection::UP: + return hon_protocol::VerticalSwingMode::UP; + case AirflowVerticalDirection::DOWN: + return hon_protocol::VerticalSwingMode::DOWN; + case AirflowVerticalDirection::HEALTH_DOWN: + return hon_protocol::VerticalSwingMode::HEALTH_DOWN; + default: + return hon_protocol::VerticalSwingMode::CENTER; + } +} + +hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) { + switch (direction) { + case AirflowHorizontalDirection::MAX_LEFT: + return hon_protocol::HorizontalSwingMode::MAX_LEFT; + case AirflowHorizontalDirection::LEFT: + return hon_protocol::HorizontalSwingMode::LEFT; + case AirflowHorizontalDirection::RIGHT: + return hon_protocol::HorizontalSwingMode::RIGHT; + case AirflowHorizontalDirection::MAX_RIGHT: + return hon_protocol::HorizontalSwingMode::MAX_RIGHT; + default: + return hon_protocol::HorizontalSwingMode::CENTER; + } +} + +HonClimate::HonClimate() + : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), + cleaning_status_(CleaningState::NO_CLEANING), + got_valid_outdoor_temp_(false), + hvac_hardware_info_available_(false), + hvac_functions_{false, false, false, false, false}, + use_crc_(hvac_functions_[2]), + active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + outdoor_sensor_(nullptr) { + this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; + this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; +} + +HonClimate::~HonClimate() {} + +void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; } + +bool HonClimate::get_beeper_state() const { return this->beeper_status_; } + +void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } + +AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; }; + +void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { + this->vertical_direction_ = direction; + this->set_force_send_control_(true); +} + +AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } + +void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { + this->horizontal_direction_ = direction; + this->set_force_send_control_(true); +} + +std::string HonClimate::get_cleaning_status_text() const { + switch (this->cleaning_status_) { + case CleaningState::SELF_CLEAN: + return "Self clean"; + case CleaningState::STERI_CLEAN: + return "56°C Steri-Clean"; + default: + return "No cleaning"; + } +} + +CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; } + +void HonClimate::start_self_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending self cleaning start request"); + this->action_request_ = ActionRequest::START_SELF_CLEAN; + this->set_force_send_control_(true); + } +} + +void HonClimate::start_steri_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending steri cleaning start request"); + this->action_request_ = ActionRequest::START_STERI_CLEAN; + this->set_force_send_control_(true); + } +} + +haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + // Should check this before preprocess + if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { + ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " + "protocol instead of hOn"); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + return haier_protocol::HandlerError::INVALID_ANSWER; + } + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { + // Wrong structure + this->set_phase(ProtocolPhases::SENDING_INIT_1); + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + // All OK + hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data; + char tmp[9]; + tmp[8] = 0; + strncpy(tmp, answr->protocol_version, 8); + this->hvac_protocol_version_ = std::string(tmp); + strncpy(tmp, answr->software_version, 8); + this->hvac_software_version_ = std::string(tmp); + strncpy(tmp, answr->hardware_version, 8); + this->hvac_hardware_version_ = std::string(tmp); + strncpy(tmp, answr->device_name, 8); + this->hvac_device_name_ = std::string(tmp); + this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support + this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->hvac_hardware_info_available_ = true; + this->set_phase(ProtocolPhases::SENDING_INIT_2); + return result; + } else { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, + (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } else { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, + (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + } else { + if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(hon_protocol::HaierPacketControl)); + } + if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || + (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || + (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { + this->set_phase(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { + this->set_phase(ProtocolPhases::IDLE); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + } + } + return result; + } else { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, + message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, + ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); + return result; + } else { + this->set_phase(ProtocolPhases::IDLE); + return result; + } +} + +haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->set_phase(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { + if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { + // Unexpected answer to request + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + } + if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { + // Don't expect this answer now + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + } + memcpy(this->active_alarms_, data + 2, 8); + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::HANDLER_OK; + } else { + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + } +} + +void HonClimate::set_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), + std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::CONTROL), + std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), + std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), + std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), + std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); +} + +void HonClimate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: hOn"); + if (this->hvac_hardware_info_available_) { + ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); + ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), + (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), + (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); + ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); + } +} + +void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { + this->hvac_hardware_info_available_ = false; + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); + this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); + } + break; + case ProtocolPhases::SENDING_INIT_2: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); + this->send_message_(DEVICEID_REQUEST, this->use_crc_); + this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER); + } + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST( + (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); + this->send_message_(STATUS_REQUEST, this->use_crc_); + this->last_status_request_ = now; + this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); + } + break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); + this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); + this->last_signal_request_ = now; + this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + } + break; + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), + this->use_crc_); + this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + } + break; + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + break; +#else + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase(ProtocolPhases::IDLE); + break; +#endif + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); + this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); + this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->first_control_attempt_) { + this->control_request_timestamp_ = now; + this->first_control_attempt_ = false; + } + if (this->is_control_message_timeout_exceeded_(now)) { + ESP_LOGW(TAG, "Sending control packet timeout!"); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->forced_request_status_ = true; + this->forced_publish_ = true; + this->set_phase(ProtocolPhases::IDLE); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + haier_protocol::HaierMessage control_message = get_control_message(); + this->send_message_(control_message, this->use_crc_); + ESP_LOGI(TAG, "Control packet sent"); + this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); + } + break; + case ProtocolPhases::SENDING_POWER_ON_COMMAND: + case ProtocolPhases::SENDING_POWER_OFF_COMMAND: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; + if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) + pwr_cmd_buf[1] = 0x01; + haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, + ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + pwr_cmd_buf, sizeof(pwr_cmd_buf)); + this->send_message_(power_cmd, this->use_crc_); + this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + } + break; + + case ProtocolPhases::WAITING_INIT_1_ANSWER: + case ProtocolPhases::WAITING_INIT_2_ANSWER: + case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + case ProtocolPhases::WAITING_STATUS_ANSWER: + case ProtocolPhases::WAITING_CONTROL_ANSWER: + case ProtocolPhases::WAITING_POWER_ON_ANSWER: + case ProtocolPhases::WAITING_POWER_OFF_ANSWER: + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); +#endif + } break; + default: + // Shouldn't get here +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); +#else + ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); +#endif + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; + } +} + +haier_protocol::HaierMessage HonClimate::get_control_message() { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + bool has_hvac_settings = false; + if (this->hvac_settings_.valid) { + has_hvac_settings = true; + HvacSettings climate_control; + climate_control = this->hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + case CLIMATE_MODE_HEAT_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + // Disabling boost and eco mode for Fan only + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + break; + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + break; + case CLIMATE_SWING_VERTICAL: + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + break; + case CLIMATE_SWING_BOTH: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + } + } + if (climate_control.target_temperature.has_value()) { + float target_temp = climate_control.target_temperature.value(); + out_data->set_point = ((int) target_temp) - 16; // set the temperature at our offset, subtract 16. + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; + } + if (out_data->ac_power == 0) { + // If AC is off - no presets allowed + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_ECO: + // Eco is not supported in Fan only mode + out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->quiet_mode = 0; + // Boost is not supported in Fan only mode + out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_AWAY: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_SLEEP: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 1; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + break; + } + } + } else { + if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO) + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + } + out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0; + control_out_buffer[4] = 0; // This byte should be cleared before setting values + out_data->display_status = this->display_status_ ? 1 : 0; + out_data->health_mode = this->health_mode_ ? 1 : 0; + switch (this->action_request_) { + case ActionRequest::START_SELF_CLEAN: + this->action_request_ = ActionRequest::NO_ACTION; + out_data->self_cleaning_status = 1; + out_data->steri_clean = 0; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + break; + case ActionRequest::START_STERI_CLEAN: + this->action_request_ = ActionRequest::NO_ACTION; + out_data->self_cleaning_status = 0; + out_data->steri_clean = 1; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + break; + default: + // No change + break; + } + return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(hon_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + hon_protocol::HaierStatus packet; + if (size < sizeof(hon_protocol::HaierStatus)) + size = sizeof(hon_protocol::HaierStatus); + memcpy(&packet, packet_buffer, size); + if (packet.sensors.error_status != 0) { + ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); + } + if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { + got_valid_outdoor_temp_ = true; + float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); + if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) + this->outdoor_sensor_->publish_state(otemp); + } + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_ECO; + } else if (packet.control.fast_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.sleep_mode != 0) { + this->preset = CLIMATE_PRESET_SLEEP; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f); + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.sensors.room_temperature / 2.0f; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) hon_protocol::FanMode::FAN_AUTO: + if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + // Shouldn't accept fan speed auto in fan-only mode even if AC reports it + ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring"); + } + break; + case (uint8_t) hon_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) hon_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) hon_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + { + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status != 0; + if (disp_status != this->display_status_) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->set_force_send_control_(true); + } else { + this->display_status_ = disp_status; + } + } + } + } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } + { + CleaningState new_cleaning; + if (packet.control.steri_clean == 1) { + // Steri-cleaning + new_cleaning = CleaningState::STERI_CLEAN; + } else if (packet.control.self_cleaning_status == 1) { + // Self-cleaning + new_cleaning = CleaningState::SELF_CLEAN; + } else { + // No cleaning + new_cleaning = CleaningState::NO_CLEANING; + } + if (new_cleaning != this->cleaning_status_) { + ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); + if (new_cleaning == CleaningState::NO_CLEANING) { + // Turning AC off after cleaning + this->action_request_ = ActionRequest::TURN_POWER_OFF; + } + this->cleaning_status_ = new_cleaning; + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) hon_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) hon_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) hon_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) hon_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) hon_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_HEAT_COOL; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) { + if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + this->swing_mode = CLIMATE_SWING_BOTH; + } else { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } + } else { + if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (this->forced_publish_ || should_publish) { +#if (HAIER_LOG_LEVEL > 4) + std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); +#endif + this->publish_state(); +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Publish delay: %lld ms", + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - + _publish_start) + .count()); +#endif + this->forced_publish_ = false; + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +bool HonClimate::is_message_invalid(uint8_t message_type) { + return message_type == (uint8_t) hon_protocol::FrameType::INVALID; +} + +void HonClimate::process_pending_action() { + switch (this->action_request_) { + case ActionRequest::START_SELF_CLEAN: + case ActionRequest::START_STERI_CLEAN: + // Will reset action with control message sending + this->set_phase(ProtocolPhases::SENDING_CONTROL); + break; + default: + HaierClimateBase::process_pending_action(); + break; + } +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h new file mode 100644 index 000000000000..cf566e3b8e0e --- /dev/null +++ b/esphome/components/haier/hon_climate.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include "esphome/components/sensor/sensor.h" +#include "haier_base.h" + +namespace esphome { +namespace haier { + +enum class AirflowVerticalDirection : uint8_t { + HEALTH_UP = 0, + MAX_UP = 1, + UP = 2, + CENTER = 3, + DOWN = 4, + HEALTH_DOWN = 5, +}; + +enum class AirflowHorizontalDirection : uint8_t { + MAX_LEFT = 0, + LEFT = 1, + CENTER = 2, + RIGHT = 3, + MAX_RIGHT = 4, +}; + +enum class CleaningState : uint8_t { + NO_CLEANING = 0, + SELF_CLEAN = 1, + STERI_CLEAN = 2, +}; + +class HonClimate : public HaierClimateBase { + public: + HonClimate(); + HonClimate(const HonClimate &) = delete; + HonClimate &operator=(const HonClimate &) = delete; + ~HonClimate(); + void dump_config() override; + void set_beeper_state(bool state); + bool get_beeper_state() const; + void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor); + AirflowVerticalDirection get_vertical_airflow() const; + void set_vertical_airflow(AirflowVerticalDirection direction); + AirflowHorizontalDirection get_horizontal_airflow() const; + void set_horizontal_airflow(AirflowHorizontalDirection direction); + std::string get_cleaning_status_text() const; + CleaningState get_cleaning_status() const; + void start_self_cleaning(); + void start_steri_cleaning(); + + protected: + void set_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + bool is_message_invalid(uint8_t message_type) override; + void process_pending_action() override; + + // Answers handlers + haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + size_t data_size); + haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + std::unique_ptr last_status_message_; + bool beeper_status_; + CleaningState cleaning_status_; + bool got_valid_outdoor_temp_; + AirflowVerticalDirection vertical_direction_; + AirflowHorizontalDirection horizontal_direction_; + bool hvac_hardware_info_available_; + std::string hvac_protocol_version_; + std::string hvac_software_version_; + std::string hvac_hardware_version_; + std::string hvac_device_name_; + bool hvac_functions_[5]; + bool &use_crc_; + uint8_t active_alarms_[8]; + esphome::sensor::Sensor *outdoor_sensor_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h new file mode 100644 index 000000000000..c6b32df200c7 --- /dev/null +++ b/esphome/components/haier/hon_packet.h @@ -0,0 +1,228 @@ +#pragma once + +#include + +namespace esphome { +namespace haier { +namespace hon_protocol { + +enum class VerticalSwingMode : uint8_t { + HEALTH_UP = 0x01, + MAX_UP = 0x02, + HEALTH_DOWN = 0x03, + UP = 0x04, + CENTER = 0x06, + DOWN = 0x08, + AUTO = 0x0C +}; + +enum class HorizontalSwingMode : uint8_t { + CENTER = 0x00, + MAX_LEFT = 0x03, + LEFT = 0x04, + RIGHT = 0x05, + MAX_RIGHT = 0x06, + AUTO = 0x07 +}; + +enum class ConditioningMode : uint8_t { + AUTO = 0x00, + COOL = 0x01, + DRY = 0x02, + HEALTHY_DRY = 0x03, + HEAT = 0x04, + ENERGY_SAVING = 0x05, + FAN = 0x06 +}; + +enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C) + // 11 + uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode + uint8_t : 0; + // 12 + uint8_t fan_mode : 3; // See enum FanMode + uint8_t special_mode : 2; // See enum SpecialMode + uint8_t ac_mode : 3; // See enum ConditioningMode + // 13 + uint8_t : 8; + // 14 + uint8_t ten_degree : 1; // 10 degree status + uint8_t display_status : 1; // If 0 disables AC's display + uint8_t half_degree : 1; // Use half degree + uint8_t intelligence_status : 1; // Intelligence status + uint8_t pmv_status : 1; // Comfort/PMV status + uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius + uint8_t : 1; + uint8_t steri_clean : 1; + // 15 + uint8_t ac_power : 1; // Is ac on or off + uint8_t health_mode : 1; // Health mode (negative ions) on or off + uint8_t electric_heating_status : 1; // Electric heating status + uint8_t fast_mode : 1; // Fast mode + uint8_t quiet_mode : 1; // Quiet mode + uint8_t sleep_mode : 1; // Sleep mode + uint8_t lock_remote : 1; // Disable remote + uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command) + // 16 + uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%) + // 17 + uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode + uint8_t : 3; + uint8_t human_sensing_status : 2; // Human sensing status + // 18 + uint8_t change_filter : 1; // Filter need replacement + uint8_t : 0; + // 19 + uint8_t fresh_air_status : 1; // Fresh air status + uint8_t humidification_status : 1; // Humidification status + uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status + uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status + uint8_t self_cleaning_status : 1; // Self cleaning status + uint8_t light_status : 1; // Light status + uint8_t energy_saving_status : 1; // Energy saving status + uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear) +}; + +struct HaierPacketSensors { + // 20 + uint8_t room_temperature; // 0.5°C step + // 21 + uint8_t room_humidity; // 0%-100% with 1% step + // 22 + uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C) + // 23 + uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple) + uint8_t : 1; + uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only) + // 24 + uint8_t error_status; // See enum ErrorStatus + // 25 + uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP) + uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan) + uint8_t : 3; + uint8_t err_confirmation : 1; // If 1 clear error status + // 26 + uint16_t total_cleaning_time; // Cleaning cumulative time (1h step) + // 28 + uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 30 + uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 32 + uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step) + // 34 + uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step) + // 36 + uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; + HaierPacketSensors sensors; +}; + +struct DeviceVersionAnswer { + char protocol_version[8]; + char software_version[8]; + uint8_t encryption[3]; + char hardware_version[8]; + uint8_t : 8; + char device_name[8]; + uint8_t functions[2]; +}; + +// In this section comments: +// - module is the ESP32 control module (communication module in Haier protocol document) +// - device is the conditioner control board (network appliances in Haier protocol document) +enum class FrameType : uint8_t { + CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required) + STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device, + // required) + INVALID = 0x03, // Communication error indication (module <-> device, required) + ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required) + CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module + // <-> device, required) + REPORT = 0x06, // Report frame (module <-> device, interactive, required) + STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required) + SYSTEM_DOWNLINK = 0x11, // System downlink frame (module -> device, optional) + DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional) + SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional) + SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional) + DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional) + DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional) + GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional) + GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required) + GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_ + GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional) + GET_ALL_ADDRESSES_RESPONSE = + 0x68, // Answer to request of all devices addresses (module <- device , interactive, optional) + HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional) + GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required) + GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required) + GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required) + GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required) + GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required) + GET_DEVICE_CONFIGURATION_RESPONSE = + 0x7D, // Response to device configuration request (module <- device, interactive, required) + DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device) + // (module -> device, interactive, optional) + UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module + // <- device, interactive, optional) + START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required) + START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required) + GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required) + GET_FIRMWARE_CONTENT_RESPONSE = + 0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?) + CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required) + CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required) + GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required) + GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required) + GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required) + GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required) + GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required) + GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required) + GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional) + GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional) + START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required) + START_WIFI_CONFIGURATION_RESPONSE = + 0xF3, // Response to start WiFi configuration request (module -> device, interactive, required) + STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required) + STOP_WIFI_CONFIGURATION_RESPONSE = + 0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required) + REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required) + CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional) + BIG_DATA_REPORT_CONFIGURATION = + 0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional) + BIG_DATA_REPORT_CONFIGURATION_RESPONSE = + 0xFB, // Response to set big data configuration (module <- device, interactive, optional) + GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required) + GET_MANAGEMENT_INFORMATION_RESPONSE = + 0xFD, // Response to management information request (module <- device, required) + WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional) +}; + +enum class SubcommandsControl : uint16_t { + GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) + GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) + GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None) + SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1 + // + parameter data1 + parameter ID2 + parameter data 2 + ...) + SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user + // data (packet content: ???) + SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID, + // the only group mentioned in document is 1) and return all user data (packet + // content: all values like in status packet) +}; + +} // namespace hon_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.cpp b/esphome/components/haier/logger_handler.cpp new file mode 100644 index 000000000000..f886318097a2 --- /dev/null +++ b/esphome/components/haier/logger_handler.cpp @@ -0,0 +1,33 @@ +#include "logger_handler.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace haier { + +void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) { + switch (level) { + case haier_protocol::HaierLogLevel::LEVEL_ERROR: + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_WARNING: + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_INFO: + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_DEBUG: + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_VERBOSE: + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message); + break; + default: + // Just ignore everything else + break; + } +} + +void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); }; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.h b/esphome/components/haier/logger_handler.h new file mode 100644 index 000000000000..2955468f37ea --- /dev/null +++ b/esphome/components/haier/logger_handler.h @@ -0,0 +1,14 @@ +#pragma once + +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +// This file is called in the code generated by python script +// Do not use it directly! +void init_haier_protocol_logging(); + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp new file mode 100644 index 000000000000..f29f840088d3 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.cpp @@ -0,0 +1,563 @@ +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "smartair2_climate.h" +#include "smartair2_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; + +Smartair2Climate::Smartair2Climate() + : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} + +haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, + (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + } else { + if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(smartair2_protocol::HaierPacketControl)); + } + if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { + this->set_phase(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { + this->set_phase(ProtocolPhases::IDLE); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + } + } + return result; + } else { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } +} + +haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION) + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + // Invalid packet is expected answer + if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && + ((data[37] & 0x04) != 0)) { + ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " + "instead of smartAir2"); + } + this->set_phase(ProtocolPhases::SENDING_INIT_2); + return haier_protocol::HandlerError::HANDLER_OK; +} + +haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + (uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->set_phase(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) { + if (this->protocol_phase_ >= ProtocolPhases::IDLE) + return HaierClimateBase::timeout_default_handler_(message_type); + this->timeouts_counter_++; + ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, + (int) this->protocol_phase_, this->timeouts_counter_); + if (this->timeouts_counter_ >= 3) { + ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); + if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) + new_phase = ProtocolPhases::SENDING_INIT_1; + this->set_phase(new_phase); + } else { + // Returning to the previous state to try again + this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1)); + } + return haier_protocol::HandlerError::HANDLER_OK; +} + +void Smartair2Climate::set_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::CONTROL), + std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), + std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_timeout_handler( + (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), + std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); + this->haier_protocol_.set_timeout_handler( + (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); + this->haier_protocol_.set_timeout_handler( + (uint8_t) (smartair2_protocol::FrameType::CONTROL), + std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); +} + +void Smartair2Climate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: smartAir2"); +} + +void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + if (this->can_send_message() && + (((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) || + ((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) { + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, + sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, false); + this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); + } + break; + case ProtocolPhases::SENDING_INIT_2: + case ProtocolPhases::WAITING_INIT_2_ANSWER: + this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, + 0x4D01); + this->send_message_(STATUS_REQUEST, false); + this->last_status_request_ = now; + this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); + } + break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + this->send_message_( + this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false); + this->last_signal_request_ = now; + this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + } + break; + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + break; +#else + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase(ProtocolPhases::IDLE); + break; +#endif + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); + break; + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->first_control_attempt_) { + this->control_request_timestamp_ = now; + this->first_control_attempt_ = false; + } + if (this->is_control_message_timeout_exceeded_(now)) { + ESP_LOGW(TAG, "Sending control packet timeout!"); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->forced_request_status_ = true; + this->forced_publish_ = true; + this->set_phase(ProtocolPhases::IDLE); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_( + now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests + { + haier_protocol::HaierMessage control_message = get_control_message(); + this->send_message_(control_message, false); + ESP_LOGI(TAG, "Control packet sent"); + this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); + } + break; + case ProtocolPhases::SENDING_POWER_ON_COMMAND: + case ProtocolPhases::SENDING_POWER_OFF_COMMAND: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + haier_protocol::HaierMessage power_cmd( + (uint8_t) smartair2_protocol::FrameType::CONTROL, + this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03); + this->send_message_(power_cmd, false); + this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + } + break; + case ProtocolPhases::WAITING_INIT_1_ANSWER: + case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: + case ProtocolPhases::WAITING_STATUS_ANSWER: + case ProtocolPhases::WAITING_CONTROL_ANSWER: + case ProtocolPhases::WAITING_POWER_ON_ANSWER: + case ProtocolPhases::WAITING_POWER_OFF_ANSWER: + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); +#endif + } break; + default: + // Shouldn't get here +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); +#else + ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); +#endif + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; + } +} + +haier_protocol::HaierMessage Smartair2Climate::get_control_message() { + uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); + smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; + out_data->cntrl = 0; + if (this->hvac_settings_.valid) { + HvacSettings climate_control; + climate_control = this->hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + + case CLIMATE_MODE_HEAT_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + break; + + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->use_swing_bits = 0; + out_data->swing_both = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_both = 0; + out_data->vertical_swing = 1; + out_data->horizontal_swing = 0; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_both = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 1; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_both = 1; + out_data->use_swing_bits = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 0; + break; + } + } + if (climate_control.target_temperature.has_value()) { + float target_temp = climate_control.target_temperature.value(); + out_data->set_point = target_temp - 16; // set the temperature with offset 16 + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; + } + if (out_data->ac_power == 0) { + // If AC is off - no presets allowed + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->turbo_mode = 1; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_COMFORT: + out_data->turbo_mode = 0; + out_data->quiet_mode = 1; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + } + } + } + out_data->display_status = this->display_status_ ? 0 : 1; + out_data->health_mode = this->health_mode_ ? 1 : 0; + return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, + sizeof(smartair2_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(smartair2_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + smartair2_protocol::HaierStatus packet; + memcpy(&packet, packet_buffer, size); + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.turbo_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_COMFORT; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f); + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.control.room_temperature; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO: + // Sometimes AC reports in fan only mode that fan speed is auto + // but never accept this value back + if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + should_publish = true; + } + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + { + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status == 0; + if (disp_status != this->display_status_) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->set_force_send_control_(true); + } else { + this->display_status_ = disp_status; + } + } + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) smartair2_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_HEAT_COOL; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (packet.control.swing_both == 0) { + if (packet.control.vertical_swing != 0) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else if (packet.control.horizontal_swing != 0) { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } else { + swing_mode = CLIMATE_SWING_BOTH; + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (this->forced_publish_ || should_publish) { +#if (HAIER_LOG_LEVEL > 4) + std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); +#endif + this->publish_state(); +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Publish delay: %lld ms", + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - + _publish_start) + .count()); +#endif + this->forced_publish_ = false; + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Vertical Swing Status = 0x%X", packet.control.vertical_swing); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +bool Smartair2Climate::is_message_invalid(uint8_t message_type) { + return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; +} + +void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) { + int old_phase = (int) this->protocol_phase_; + int new_phase = (int) phase; + int min_p = std::min(old_phase, new_phase); + int max_p = std::max(old_phase, new_phase); + if ((min_p % 2 != 0) || (max_p - min_p > 1)) + this->timeouts_counter_ = 0; + HaierClimateBase::set_phase(phase); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h new file mode 100644 index 000000000000..f173b1074955 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "haier_base.h" + +namespace esphome { +namespace haier { + +class Smartair2Climate : public HaierClimateBase { + public: + Smartair2Climate(); + Smartair2Climate(const Smartair2Climate &) = delete; + Smartair2Climate &operator=(const Smartair2Climate &) = delete; + ~Smartair2Climate(); + void dump_config() override; + + protected: + void set_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + bool is_message_invalid(uint8_t message_type) override; + void set_phase(HaierClimateBase::ProtocolPhases phase) override; + // Answer and timeout handlers + haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + size_t data_size); + haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + std::unique_ptr last_status_message_; + unsigned int timeouts_counter_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h new file mode 100644 index 000000000000..f791c21af2bc --- /dev/null +++ b/esphome/components/haier/smartair2_packet.h @@ -0,0 +1,100 @@ +#pragma once + +namespace esphome { +namespace haier { +namespace smartair2_protocol { + +enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t : 8; // Temperature high byte + // 11 + uint8_t room_temperature; // current room temperature 1°C step + // 12 + uint8_t : 8; // Humidity high byte + // 13 + uint8_t room_humidity; // Humidity 0%-100% with 1% step + // 14 + uint8_t : 8; + // 15 + uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00 + // 16 + uint8_t : 8; + // 17 + uint8_t : 8; + // 18 + uint8_t : 8; + // 19 + uint8_t : 8; + // 20 + uint8_t : 8; + // 21 + uint8_t ac_mode; // See enum ConditioningMode + // 22 + uint8_t : 8; + // 23 + uint8_t fan_mode; // See enum FanMode + // 24 + uint8_t : 8; + // 25 + uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define + // vertical/horizontal/off + // 26 + uint8_t : 3; + uint8_t use_fahrenheit : 1; + uint8_t : 3; + uint8_t lock_remote : 1; // Disable remote + // 27 + uint8_t ac_power : 1; // Is ac on or off + uint8_t : 2; + uint8_t health_mode : 1; // Health mode on or off + uint8_t compressor : 1; // Compressor on or off ??? + uint8_t half_degree : 1; // Use half degree + uint8_t ten_degree : 1; // 10 degree status (only work in heat mode) + uint8_t : 0; + // 28 + uint8_t : 8; + // 29 + uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used + uint8_t turbo_mode : 1; // Turbo mode + uint8_t quiet_mode : 1; // Sleep mode + uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0) + uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 => + // swing off + uint8_t display_status : 1; // Led on or off + uint8_t : 0; + // 30 + uint8_t : 8; + // 31 + uint8_t : 8; + // 32 + uint8_t : 8; // Target temperature high byte + // 33 + uint8_t set_point; // Target temperature with 16°C offset, 1°C step +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; +}; + +enum class FrameType : uint8_t { + CONTROL = 0x01, + STATUS = 0x02, + INVALID = 0x03, + CONFIRM = 0x05, + GET_DEVICE_VERSION = 0x61, + GET_DEVICE_VERSION_RESPONSE = 0x62, + GET_DEVICE_ID = 0x70, + GET_DEVICE_ID_RESPONSE = 0x71, + REPORT_NETWORK_STATUS = 0xF7, + NO_COMMAND = 0xFF, +}; + +} // namespace smartair2_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index d7c8d544f942..66b72f9e3e9f 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -6,6 +6,9 @@ CONF_CURRENT, CONF_FREQUENCY, CONF_ID, + CONF_PHASE_A, + CONF_PHASE_B, + CONF_PHASE_C, CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, @@ -24,9 +27,6 @@ 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" diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index cc8e75dcbd39..a043b4a61b4b 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -33,6 +33,7 @@ "greeya": Protocol.PROTOCOL_GREEYAA, "greeyan": Protocol.PROTOCOL_GREEYAN, "greeyac": Protocol.PROTOCOL_GREEYAC, + "greeyt": Protocol.PROTOCOL_GREEYT, "hisense_aud": Protocol.PROTOCOL_HISENSE_AUD, "hitachi": Protocol.PROTOCOL_HITACHI, "hyundai": Protocol.PROTOCOL_HYUNDAI, @@ -115,7 +116,7 @@ def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.20") + cg.add_library("tonia/HeatpumpIR", "1.0.23") if CORE.is_esp8266 or CORE.is_esp32: cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index bed1dc76c06b..5e7237b63c8a 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -27,6 +27,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT + {PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT {PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT {PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT {PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index c60b9441114b..e8b03b4c26d8 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -27,6 +27,7 @@ enum Protocol { PROTOCOL_GREEYAA, PROTOCOL_GREEYAN, PROTOCOL_GREEYAC, + PROTOCOL_GREEYT, PROTOCOL_HISENSE_AUD, PROTOCOL_HITACHI, PROTOCOL_HYUNDAI, diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 7b93b00503ab..2825e4f04cf1 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -12,7 +12,7 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); // Calculate the mask & clear the space for the data. // Clear the destination bits. - *dst &= ~(uint8_t)(mask << offset); + *dst &= ~(uint8_t) (mask << offset); // Merge in the data. *dst |= ((data & mask) << offset); } diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 65cfaa417501..0bfc3ae564c1 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -12,7 +12,7 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); // Calculate the mask & clear the space for the data. // Clear the destination bits. - *dst &= ~(uint8_t)(mask << offset); + *dst &= ~(uint8_t) (mask << offset); // Merge in the data. *dst |= ((data & mask) << offset); } diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index ecdaa07ab2ac..1a9f47faafab 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -38,7 +38,7 @@ void HLW8012Component::dump_config() { LOG_PIN(" SEL Pin: ", this->sel_pin_) LOG_PIN(" CF Pin: ", this->cf_pin_) LOG_PIN(" CF1 Pin: ", this->cf1_pin_) - ESP_LOGCONFIG(TAG, " Change measurement mode every %u", this->change_mode_every_); + ESP_LOGCONFIG(TAG, " Change measurement mode every %" PRIu32, this->change_mode_every_); ESP_LOGCONFIG(TAG, " Current resistor: %.1f mΩ", this->current_resistor_ * 1000.0f); ESP_LOGCONFIG(TAG, " Voltage Divider: %.1f", this->voltage_divider_); LOG_UPDATE_INTERVAL(this) diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index adb49ffb66e3..312391f533a6 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -5,6 +5,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/pulse_counter/pulse_counter_sensor.h" +#include + namespace esphome { namespace hlw8012 { diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 8e9ee4c6fbe8..27af0b5b6b48 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -16,6 +16,7 @@ ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@freekode"] hm3301_ns = cg.esphome_ns.namespace("hm3301") HM3301Component = hm3301_ns.class_( diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 26e8e2b60c4b..7edd13965e7d 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -3,6 +3,9 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ADDRESS, + CONF_FIELD_STRENGTH_X, + CONF_FIELD_STRENGTH_Y, + CONF_FIELD_STRENGTH_Z, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, @@ -18,9 +21,6 @@ hmc5883l_ns = cg.esphome_ns.namespace("hmc5883l") -CONF_FIELD_STRENGTH_X = "field_strength_x" -CONF_FIELD_STRENGTH_Y = "field_strength_y" -CONF_FIELD_STRENGTH_Z = "field_strength_z" CONF_HEADING = "heading" HMC5883LComponent = hmc5883l_ns.class_( diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index f5e73c8854cb..35e660f7c11f 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -12,7 +12,7 @@ void HomeassistantSensor::setup() { this->entity_id_, this->attribute_, [this](const std::string &state) { auto val = parse_number(state); if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); this->publish_state(NAN); return; } diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index 910c39e8c88e..124bd6bb95a3 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,9 +35,9 @@ uint8_t HONEYWELLABPSensor::readsensor_() { // if device is normal and there is new data, bitmask and save the raw data if (status_ == 0) { // 14 - bit pressure is the last 6 bits of byte 0 (high bits) & all of byte 1 (lowest 8 bits) - pressure_count_ = ((uint16_t)(buf_[0]) << 8 & 0x3F00) | ((uint16_t)(buf_[1]) & 0xFF); + pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 - temperature_count_ = (((uint16_t)(buf_[2]) << 3) & 0x7F8) | (((uint16_t)(buf_[3]) >> 5) & 0x7); + temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); ESP_LOGV(TAG, "Sensor pressure_count_ %d", pressure_count_); ESP_LOGV(TAG, "Sensor temperature_count_ %d", temperature_count_); } diff --git a/esphome/components/honeywellabp/sensor.py b/esphome/components/honeywellabp/sensor.py index 720a96b93c2c..ed8bff6e9b4e 100644 --- a/esphome/components/honeywellabp/sensor.py +++ b/esphome/components/honeywellabp/sensor.py @@ -52,7 +52,6 @@ async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py new file mode 100644 index 000000000000..14d259786638 --- /dev/null +++ b/esphome/components/host/__init__.py @@ -0,0 +1,39 @@ +from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_HOST, +) +from esphome.core import CORE +import esphome.config_validation as cv +import esphome.codegen as cg + +from .const import KEY_HOST + +# force import gpio to register pin schema +from .gpio import host_pin_to_code # noqa + + +CODEOWNERS = ["@esphome/core"] +AUTO_LOAD = ["network"] + + +def set_core_data(config): + CORE.data[KEY_HOST] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(1, 0, 0) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + set_core_data, +) + + +async def to_code(config): + cg.add_build_flag("-DUSE_HOST") + cg.add_define("ESPHOME_BOARD", "host") + cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/host/const.py b/esphome/components/host/const.py new file mode 100644 index 000000000000..b6f4c4e27751 --- /dev/null +++ b/esphome/components/host/const.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +KEY_HOST = "host" + +host_ns = cg.esphome_ns.namespace("host") diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp new file mode 100644 index 000000000000..164d622dd4ab --- /dev/null +++ b/esphome/components/host/core.cpp @@ -0,0 +1,77 @@ +#ifdef USE_HOST + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" + +#include +#include +#include +#include + +namespace esphome { + +void IRAM_ATTR HOT yield() { ::sched_yield(); } +uint32_t IRAM_ATTR HOT millis() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t ms = round(spec.tv_nsec / 1e6); + return ((uint32_t) seconds) * 1000U + ms; +} +void IRAM_ATTR HOT delay(uint32_t ms) { + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000; + int res; + do { + res = nanosleep(&ts, &ts); + } while (res != 0 && errno == EINTR); +} +uint32_t IRAM_ATTR HOT micros() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t us = round(spec.tv_nsec / 1e3); + return ((uint32_t) seconds) * 1000000U + us; +} +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { + struct timespec ts; + ts.tv_sec = us / 1000000U; + ts.tv_nsec = (us % 1000000U) * 1000U; + int res; + do { + res = nanosleep(&ts, &ts); + } while (res != 0 && errno == EINTR); +} +void arch_restart() { exit(0); } +void arch_init() { + // pass +} +void IRAM_ATTR HOT arch_feed_wdt() { + // pass +} + +uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint32_t arch_get_cpu_cycle_count() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t us = spec.tv_nsec; + return ((uint32_t) seconds) * 1000000000U + us; +} +uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } + +} // namespace esphome + +void setup(); +void loop(); +int main() { + esphome::host::setup_preferences(); + setup(); + while (true) { + loop(); + } +} + +#endif // USE_HOST diff --git a/esphome/components/host/gpio.cpp b/esphome/components/host/gpio.cpp new file mode 100644 index 000000000000..e46f158513a9 --- /dev/null +++ b/esphome/components/host/gpio.cpp @@ -0,0 +1,59 @@ +#ifdef USE_HOST + +#include "gpio.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace host { + +static const char *const TAG = "host"; + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin HostGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type); +} +void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); } + +std::string HostGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +bool HostGPIOPin::digital_read() { return inverted_; } +void HostGPIOPin::digital_write(bool value) { + // pass + ESP_LOGD(TAG, "Setting pin %d to %s", pin_, value != inverted_ ? "HIGH" : "LOW"); +} +void HostGPIOPin::detach_interrupt() const {} + +} // namespace host + +using namespace host; + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + auto *arg = reinterpret_cast(arg_); + return arg->inverted; +} +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + // pass +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + auto *arg = reinterpret_cast(arg_); + ESP_LOGD(TAG, "Clearing interrupt for pin %d", arg->pin); +} + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h new file mode 100644 index 000000000000..c0920467d65d --- /dev/null +++ b/esphome/components/host/gpio.h @@ -0,0 +1,37 @@ +#pragma once + +#ifdef USE_HOST + +#include "esphome/core/hal.h" + +namespace esphome { +namespace host { + +class HostGPIOPin : public InternalGPIOPin { + public: + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + void setup() override { pin_mode(flags_); } + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + void detach_interrupt() const override; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return pin_; } + bool is_inverted() const override { return inverted_; } + + protected: + void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; + + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace host +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py new file mode 100644 index 000000000000..d523d28ee501 --- /dev/null +++ b/esphome/components/host/gpio.py @@ -0,0 +1,73 @@ +import logging + +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +from esphome import pins +import esphome.config_validation as cv +import esphome.codegen as cg + +from .const import host_ns + + +_LOGGER = logging.getLogger(__name__) + + +HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith("GPIO"): + return cv.int_(value[len("GPIO") :].strip()) + return value + + +def validate_gpio_pin(value): + return _translate_pin(value) + + +HOST_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(HostGPIOPin), + cv.Required(CONF_NUMBER): validate_gpio_pin, + cv.Optional(CONF_MODE, default={}): cv.Schema( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, + } + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + }, +) + + +@pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA) +async def host_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp new file mode 100644 index 000000000000..bf45893e40ca --- /dev/null +++ b/esphome/components/host/preferences.cpp @@ -0,0 +1,36 @@ +#ifdef USE_HOST + +#include "preferences.h" +#include +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace host { + +static const char *const TAG = "host.preferences"; + +class HostPreferences : public ESPPreferences { + public: + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { return {}; } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { return {}; } + + bool sync() override { return true; } + bool reset() override { return true; } +}; + +void setup_preferences() { + auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) + global_preferences = pref; +} + +} // namespace host + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h new file mode 100644 index 000000000000..7462360ec3f8 --- /dev/null +++ b/esphome/components/host/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_HOST + +namespace esphome { +namespace host { + +void setup_preferences(); + +} // namespace host +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index bd1c82c96b2a..b56e96badcff 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -1,9 +1,10 @@ // Official Datasheet: -// https://www.maxbotix.com/documents/HRXL-MaxSonar-WR_Datasheet.pdf +// HRXL: https://www.maxbotix.com/documents/HRXL-MaxSonar-WR_Datasheet.pdf +// XL: https://www.maxbotix.com/documents/XL-MaxSonar-WR_Datasheet.pdf // // This implementation is designed to work with the TTL Versions of the -// MaxBotix HRXL MaxSonar WR sensor series. The sensor's TTL Pin (5) should be -// wired to one of the ESP's input pins and configured as uart rx_pin. +// MaxBotix HRXL and XL MaxSonar WR sensor series. The sensor's TTL Pin (5) +// should be wired to one of the ESP's input pins and configured as uart rx_pin. #include "hrxl_maxsonar_wr.h" #include "esphome/core/log.h" @@ -17,8 +18,10 @@ static const uint8_t ASCII_NBSP = 0xFF; static const int MAX_DATA_LENGTH_BYTES = 6; /** - * The sensor outputs something like "R1234\r" at a fixed rate of 6 Hz. Where - * 1234 means a distance of 1,234 m. + * HRXL sensors output the format "R1234\r" at 6Hz + * The 1234 means 1234mm + * XL sensors output the format "R123\r" at 5 to 10Hz + * The 123 means 123cm */ void HrxlMaxsonarWrComponent::loop() { uint8_t data; @@ -42,9 +45,17 @@ void HrxlMaxsonarWrComponent::check_buffer_() { if (this->buffer_.back() == static_cast(ASCII_CR) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) { ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.c_str()); - if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && - this->buffer_.back() == static_cast(ASCII_CR)) { - int millimeters = parse_number(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2)).value_or(0); + size_t rpos = this->buffer_.find(static_cast(ASCII_CR)); + + if (this->buffer_.length() <= MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && rpos != std::string::npos) { + std::string distance = this->buffer_.substr(1, rpos - 1); + int millimeters = parse_number(distance).value_or(0); + + // XL reports in cm instead of mm and reports 3 digits instead of 4 + if (distance.length() == 3) { + millimeters = millimeters * 10; + } + float meters = float(millimeters) / 1000.0; ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); this->publish_state(meters); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 0958c0768328..b885de18e6c7 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -80,8 +80,6 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(std::string, body) - TEMPLATABLE_VALUE(const char *, useragent) - TEMPLATABLE_VALUE(uint16_t, timeout) void add_header(const char *key, TemplatableValue value) { this->headers_.insert({key, value}); } @@ -105,25 +103,18 @@ template class HttpRequestSendAction : public Action { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); this->parent_->set_body(json::build_json(f)); } - if (this->useragent_.has_value()) { - this->parent_->set_useragent(this->useragent_.value(x...)); - } - if (this->timeout_.has_value()) { - this->parent_->set_timeout(this->timeout_.value(x...)); - } - if (!this->headers_.empty()) { - std::list
headers; - for (const auto &item : this->headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - headers.push_back(header); - } - this->parent_->set_headers(headers); + std::list
headers; + for (const auto &item : this->headers_) { + auto val = item.second; + Header header; + header.name = item.first; + header.value = val.value(x...); + headers.push_back(header); } + this->parent_->set_headers(headers); this->parent_->send(this->response_triggers_); this->parent_->close(); + this->parent_->set_body(""); } protected: diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index a38ec7301942..a8133ae32e3c 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -11,7 +11,11 @@ static const uint8_t HTU21D_ADDRESS = 0x40; static const uint8_t HTU21D_REGISTER_RESET = 0xFE; static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3; static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5; +static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */ static const uint8_t HTU21D_REGISTER_STATUS = 0xE7; +static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */ +static const uint8_t HTU21D_READHEATER_REG_CMD = 0x11; /**< Read Heater Control Register */ +static const uint8_t HTU21D_REG_HTRE_BIT = 0x02; /**< Control Register Heater Bit */ void HTU21DComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HTU21D..."); @@ -62,14 +66,66 @@ void HTU21DComponent::update() { raw_humidity = i2c::i2ctohs(raw_humidity); float humidity = (float(raw_humidity & 0xFFFC)) * 125.0f / 65536.0f - 6.0f; - ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); + + int8_t heater_level = this->get_heater_level(); + + ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%% Heater Level=%d", temperature, humidity, heater_level); if (this->temperature_ != nullptr) this->temperature_->publish_state(temperature); if (this->humidity_ != nullptr) this->humidity_->publish_state(humidity); + if (this->heater_ != nullptr) + this->heater_->publish_state(heater_level); this->status_clear_warning(); } + +bool HTU21DComponent::is_heater_enabled() { + uint8_t raw_heater; + if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + raw_heater = i2c::i2ctohs(raw_heater); + return (bool) (((raw_heater) >> (HTU21D_REG_HTRE_BIT)) & 0x01); +} + +void HTU21DComponent::set_heater(bool status) { + uint8_t raw_heater; + if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + raw_heater = i2c::i2ctohs(raw_heater); + if (status) { + raw_heater |= (1 << (HTU21D_REG_HTRE_BIT)); + } else { + raw_heater &= ~(1 << (HTU21D_REG_HTRE_BIT)); + } + + if (this->write_register(HTU21D_WRITERHT_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } +} + +void HTU21DComponent::set_heater_level(uint8_t level) { + if (this->write_register(HTU21D_WRITEHEATER_REG_CMD, &level, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } +} + +int8_t HTU21DComponent::get_heater_level() { + int8_t raw_heater; + if (this->read_register(HTU21D_READHEATER_REG_CMD, reinterpret_cast(&raw_heater), 2) != i2c::ERROR_OK) { + this->status_set_warning(); + return 0; + } + raw_heater = i2c::i2ctohs(raw_heater); + return raw_heater; +} + float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } } // namespace htu21d diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index a408f06d01e6..a77a8e3ada3f 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/automation.h" namespace esphome { namespace htu21d { @@ -11,6 +12,7 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { public: void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_heater(sensor::Sensor *heater) { heater_ = heater; } /// Setup (reset) the sensor and check connection. void setup() override; @@ -18,11 +20,39 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { /// Update the sensor values (temperature+humidity). void update() override; + bool is_heater_enabled(); + void set_heater(bool status); + void set_heater_level(uint8_t level); + int8_t get_heater_level(); + float get_setup_priority() const override; protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *heater_{nullptr}; +}; + +template class SetHeaterLevelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, level) + + void play(Ts... x) override { + auto level = this->level_.value(x...); + + this->parent_->set_heater_level(level); + } +}; + +template class SetHeaterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, status) + + void play(Ts... x) override { + auto status = this->status_.value(x...); + + this->parent_->set_heater(status); + } }; } // namespace htu21d diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 37422f03294b..1f878230f859 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor +from esphome import automation from esphome.const import ( CONF_HUMIDITY, CONF_ID, @@ -10,6 +11,10 @@ STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, + CONF_HEATER, + UNIT_EMPTY, + CONF_LEVEL, + CONF_STATUS, ) DEPENDENCIES = ["i2c"] @@ -19,22 +24,31 @@ "HTU21DComponent", cg.PollingComponent, i2c.I2CDevice ) +SetHeaterLevelAction = htu21d_ns.class_("SetHeaterLevelAction", automation.Action) +SetHeaterAction = htu21d_ns.class_("SetHeaterAction", automation.Action) + + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HTU21DComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + 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, ), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_HEATER): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), } ) .extend(cv.polling_component_schema("60s")) @@ -54,3 +68,45 @@ async def to_code(config): if CONF_HUMIDITY in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) + + if CONF_HEATER in config: + sens = await sensor.new_sensor(config[CONF_HEATER]) + cg.add(var.set_heater(sens)) + + +@automation.register_action( + "htu21d.set_heater_level", + SetHeaterLevelAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HTU21DComponent), + cv.Required(CONF_LEVEL): cv.templatable(cv.int_), + }, + key=CONF_LEVEL, + ), +) +async def set_heater_level_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + level_ = await cg.templatable(config[CONF_LEVEL], args, int) + cg.add(var.set_level(level_)) + return var + + +@automation.register_action( + "htu21d.set_heater", + SetHeaterAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HTU21DComponent), + cv.Required(CONF_STATUS): cv.templatable(cv.boolean), + }, + key=CONF_STATUS, + ), +) +async def set_heater_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + status_ = await cg.templatable(config[CONF_LEVEL], args, bool) + cg.add(var.set_status(status_)) + return var diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 62adc4ae86ba..dbbf4c91f467 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -28,7 +28,7 @@ void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { int32_t value = static_cast(result); - ESP_LOGD(TAG, "'%s': Got value %d", this->name_.c_str(), value); + ESP_LOGD(TAG, "'%s': Got value %" PRId32, this->name_.c_str(), value); this->publish_state(value); } } diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index 9fef649b0371..0cb6868ab5bf 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -4,6 +4,8 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" +#include + namespace esphome { namespace hx711 { diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index da4345e136be..58e00ba7a5bb 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -25,6 +25,10 @@ void HydreonRGxxComponent::dump_config() { LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \ } HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, ); + + if (this->model_ == RG9) { + ESP_LOGCONFIG(TAG, "disable_led: %s", TRUEFALSE(this->disable_led_)); + } } void HydreonRGxxComponent::setup() { @@ -187,7 +191,20 @@ void HydreonRGxxComponent::process_line_() { this->cancel_interval("reboot"); this->no_response_count_ = 0; ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); - this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode + + if (this->model_ == RG15) { + this->write_str("P\nH\nM\n"); // set sensor to (P)polling mode, (H)high res mode, (M)metric mode + } + + if (this->model_ == RG9) { + this->write_str("P\n"); // set sensor to (P)polling mode + + if (this->disable_led_) { + this->write_str("D 1\n"); // set sensor (D 1)rain detection LED disabled + } else { + this->write_str("D 0\n"); // set sensor (D 0)rain detection LED enabled + } + } return; } if (this->buffer_starts_with_("SW")) { @@ -227,7 +244,22 @@ void HydreonRGxxComponent::process_line_() { if (n == std::string::npos) { continue; } - float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr); + + if (n == this->buffer_.find('t', n)) { + // The device temperature ('t') response contains both °C and °F values: + // "t 72F 22C". + // ESPHome uses only °C, only parse °C value (move past 'F'). + n = this->buffer_.find('F', n); + if (n == std::string::npos) { + continue; + } + n += 1; // move past 'F' + } else { + n += strlen(PROTOCOL_NAMES[i]); // move past protocol name + } + + // parse value, starting at str position n + float data = strtof(this->buffer_.substr(n).c_str(), nullptr); this->sensors_[i]->publish_state(data); ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state()); this->sensors_received_ |= (1 << i); diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index 34b9bd8d5eec..1edda5980062 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -49,6 +49,8 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { float get_setup_priority() const override; + void set_disable_led(bool disable_led) { this->disable_led_ = disable_led; } + protected: void process_line_(); void schedule_reboot_(); @@ -72,6 +74,7 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { bool lens_bad_ = false; bool em_sat_ = false; bool request_temperature_ = false; + bool disable_led_ = false; // bit field showing which sensors we have received data for int sensors_received_ = -1; diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index c2dbbd673711..0fc380f9590d 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -25,12 +25,17 @@ CONF_TOTAL_ACC = "total_acc" CONF_R_INT = "r_int" +CONF_DISABLE_LED = "disable_led" + RG_MODELS = { "RG_9": RGModel.RG9, "RG_15": RGModel.RG15, - # https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf - # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf - # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf + # RG-15 + # 1.000 - https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf + # RG-9 + # 1.000 - https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf + # 1.100 - https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf + # 1.200 - https://rainsensors.com/wp-content/uploads/sites/3/2022/03/2022.02.17-rev-1.200-rg-9_instructions.pdf } SUPPORTED_SENSORS = { CONF_ACC: ["RG_15"], @@ -39,6 +44,7 @@ CONF_R_INT: ["RG_15"], CONF_MOISTURE: ["RG_9"], CONF_TEMPERATURE: ["RG_9"], + CONF_DISABLE_LED: ["RG_9"], } PROTOCOL_NAMES = { CONF_MOISTURE: "R", @@ -105,6 +111,7 @@ def _validate(config): icon=ICON_THERMOMETER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_DISABLE_LED): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -132,3 +139,6 @@ async def to_code(config): cg.add(var.set_sensor(sens, i)) cg.add(var.set_request_temperature(CONF_TEMPERATURE in config)) + + if CONF_DISABLE_LED in config: + cg.add(var.set_disable_led(config[CONF_DISABLE_LED])) diff --git a/esphome/components/hyt271/__init__.py b/esphome/components/hyt271/__init__.py new file mode 100644 index 000000000000..2e88d4f3660d --- /dev/null +++ b/esphome/components/hyt271/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Philippe12"] diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp new file mode 100644 index 000000000000..3b81294cfcdd --- /dev/null +++ b/esphome/components/hyt271/hyt271.cpp @@ -0,0 +1,52 @@ +#include "hyt271.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace hyt271 { + +static const char *const TAG = "hyt271"; + +static const uint8_t HYT271_ADDRESS = 0x28; + +void HYT271Component::dump_config() { + ESP_LOGCONFIG(TAG, "HYT271:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); +} +void HYT271Component::update() { + uint8_t raw_data[4] = {0, 0, 0, 0}; + + if (this->write(&raw_data[0], 0) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Communication with HYT271 failed! => Ask new values"); + return; + } + this->set_timeout("wait_convert", 50, [this]() { + uint8_t raw_data[4]; + if (this->read(raw_data, 4) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Communication with HYT271 failed! => Read values"); + return; + } + uint16_t raw_temperature = ((raw_data[2] << 8) | raw_data[3]) >> 2; + uint16_t raw_humidity = ((raw_data[0] & 0x3F) << 8) | raw_data[1]; + + float temperature = ((float(raw_temperature)) * (165.0f / 16383.0f)) - 40.0f; + float humidity = (float(raw_humidity)) * (100.0f / 16383.0f); + + ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); + + if (this->temperature_ != nullptr) + this->temperature_->publish_state(temperature); + if (this->humidity_ != nullptr) + this->humidity_->publish_state(humidity); + this->status_clear_warning(); + }); +} +float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace hyt271 +} // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h new file mode 100644 index 000000000000..64f32a651c1b --- /dev/null +++ b/esphome/components/hyt271/hyt271.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hyt271 { + +class HYT271Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + + void dump_config() override; + /// Update the sensor values (temperature+humidity). + void update() override; + + float get_setup_priority() const override; + + protected: + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; +}; + +} // namespace hyt271 +} // namespace esphome diff --git a/esphome/components/hyt271/sensor.py b/esphome/components/hyt271/sensor.py new file mode 100644 index 000000000000..2ec2836461a7 --- /dev/null +++ b/esphome/components/hyt271/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hyt271_ns = cg.esphome_ns.namespace("hyt271") +HYT271Component = hyt271_ns.class_( + "HYT271Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HYT271Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index a04e63e7897f..676190b0e599 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -12,6 +12,9 @@ CONF_SDA, CONF_ADDRESS, CONF_I2C_ID, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, ) from esphome.core import coroutine_with_priority, CORE @@ -42,23 +45,26 @@ def _bus_declare_type(value): ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): _bus_declare_type, - cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, - cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean - ), - cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, - cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean - ), - cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( - cv.frequency, cv.Range(min=0, min_included=False) - ), - cv.Optional(CONF_SCAN, default=True): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): _bus_declare_type, + cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( + cv.only_with_esp_idf, cv.boolean + ), + cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( + cv.only_with_esp_idf, cv.boolean + ), + cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( + cv.frequency, cv.Range(min=0, min_included=False) + ), + cv.Optional(CONF_SCAN, default=True): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), +) @coroutine_with_priority(1.0) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 82ab7bd09a3b..2b2190d28b58 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -7,6 +7,48 @@ namespace i2c { static const char *const TAG = "i2c"; +ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { + ErrorCode err = this->write(&a_register, 1, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + +ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { + WriteBuffer buffers[2]; + buffers[0].data = &a_register; + buffers[0].len = 1; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + WriteBuffer buffers[2]; + buffers[0].data = reinterpret_cast(&a_register); + buffers[0].len = 2; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + +bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { + if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) + return false; + for (size_t i = 0; i < len; i++) + data[i] = i2ctohs(data[i]); + return true; +} + bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; @@ -36,5 +78,26 @@ uint8_t I2CRegister::get() const { return value; } +I2CRegister16 &I2CRegister16::operator=(uint8_t value) { + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator&=(uint8_t value) { + value &= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator|=(uint8_t value) { + value |= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} + +uint8_t I2CRegister16::get() const { + uint8_t value = 0x00; + this->parent_->read_register16(this->register_, &value, 1); + return value; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index ffc0dadf8145..eb5d463b65e5 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -31,6 +31,25 @@ class I2CRegister { uint8_t register_; }; +class I2CRegister16 { + public: + I2CRegister16 &operator=(uint8_t value); + I2CRegister16 &operator&=(uint8_t value); + I2CRegister16 &operator|=(uint8_t value); + + explicit operator uint8_t() const { return get(); } + + uint8_t get() const; + + protected: + friend class I2CDevice; + + I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} + + I2CDevice *parent_; + uint16_t register_; +}; + // like ntohs/htons but without including networking headers. // ("i2c" byte order is big-endian) inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } @@ -44,24 +63,15 @@ class I2CDevice { void set_i2c_bus(I2CBus *bus) { bus_ = bus; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) { - ErrorCode err = this->write(&a_register, 1, stop); - if (err != ERROR_OK) - return err; - return this->read(data, len); - } + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) { - WriteBuffer buffers[2]; - buffers[0].data = &a_register; - buffers[0].len = 1; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); - } + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); // Compat APIs @@ -85,13 +95,7 @@ class I2CDevice { return res; } - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { - if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) - return false; - for (size_t i = 0; i < len; i++) - data[i] = i2ctohs(data[i]); - return true; - } + bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { return read_register(a_register, data, 1, stop) == ERROR_OK; diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 16d89c345005..d80ab1fd1d1b 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -3,6 +3,7 @@ #include "i2c_bus_arduino.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include #include @@ -154,18 +155,25 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn } } uint8_t status = wire_->endTransmission(stop); - if (status == 0) { - return ERROR_OK; - } else if (status == 1) { - // transmit buffer not large enough - ESP_LOGVV(TAG, "TX failed: buffer not large enough"); - return ERROR_UNKNOWN; - } else if (status == 2 || status == 3) { - ESP_LOGVV(TAG, "TX failed: not acknowledged"); - return ERROR_NOT_ACKNOWLEDGED; + switch (status) { + case 0: + return ERROR_OK; + case 1: + // transmit buffer not large enough + ESP_LOGVV(TAG, "TX failed: buffer not large enough"); + return ERROR_UNKNOWN; + case 2: + case 3: + ESP_LOGVV(TAG, "TX failed: not acknowledged"); + return ERROR_NOT_ACKNOWLEDGED; + case 5: + ESP_LOGVV(TAG, "TX failed: timeout"); + return ERROR_UNKNOWN; + case 4: + default: + ESP_LOGVV(TAG, "TX failed: unknown error %u", status); + return ERROR_UNKNOWN; } - ESP_LOGVV(TAG, "TX failed: unknown error %u", status); - return ERROR_UNKNOWN; } /// Perform I2C bus recovery, see: @@ -220,10 +228,14 @@ void ArduinoI2CBus::recover_() { // When SCL is kept LOW at this point, we might be looking at a device // that applies clock stretching. Wait for the release of the SCL line, // but not forever. There is no specification for the maximum allowed - // time. We'll stick to 500ms here. - auto wait = 20; + // time. We yield and reset the WDT, so as to avoid triggering reset. + // No point in trying to recover the bus by forcing a uC reset. Bus + // should recover in a few ms or less else not likely to recovery at + // all. + auto wait = 250; while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT - delay(25); + App.feed_wdt(); + delayMicroseconds(half_period_usec * 2); } if (digitalRead(scl_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 1e2a7304f238..5d35c1968b51 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -4,7 +4,9 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include +#include namespace esphome { namespace i2c { @@ -12,8 +14,20 @@ namespace i2c { static const char *const TAG = "i2c.idf"; void IDFI2CBus::setup() { - static i2c_port_t next_port = 0; - port_ = next_port++; + ESP_LOGCONFIG(TAG, "Setting up I2C bus..."); + static i2c_port_t next_port = I2C_NUM_0; + port_ = next_port; +#if I2C_NUM_MAX > 1 + next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; +#else + next_port = I2C_NUM_MAX; +#endif + + if (port_ == I2C_NUM_MAX) { + ESP_LOGE(TAG, "Too many I2C buses configured"); + this->mark_failed(); + return; + } recover_(); @@ -47,7 +61,7 @@ void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_); ESP_LOGCONFIG(TAG, " SCL Pin: GPIO%u", this->scl_pin_); - ESP_LOGCONFIG(TAG, " Frequency: %u Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " Frequency: %" PRIu32 " Hz", this->frequency_); switch (this->recovery_result_) { case RECOVERY_COMPLETED: ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); @@ -188,11 +202,13 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b return ERROR_UNKNOWN; } } - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; + if (stop) { + err = i2c_master_stop(cmd); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } } err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); i2c_cmd_link_delete(cmd); @@ -272,10 +288,14 @@ void IDFI2CBus::recover_() { // When SCL is kept LOW at this point, we might be looking at a device // that applies clock stretching. Wait for the release of the SCL line, // but not forever. There is no specification for the maximum allowed - // time. We'll stick to 500ms here. - auto wait = 20; + // time. We yield and reset the WDT, so as to avoid triggering reset. + // No point in trying to recover the bus by forcing a uC reset. Bus + // should recover in a few ms or less else not likely to recovery at + // all. + auto wait = 250; while (wait-- && gpio_get_level(scl_pin) == 0) { - delay(25); + App.feed_wdt(); + delayMicroseconds(half_period_usec * 2); } if (gpio_get_level(scl_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index e69de29bb2d1..d72e13630f22 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -0,0 +1,75 @@ +import esphome.config_validation as cv +import esphome.final_validate as fv +import esphome.codegen as cg + +from esphome import pins +from esphome.const import CONF_ID +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] +MULTI_CONF = True + +CONF_I2S_DOUT_PIN = "i2s_dout_pin" +CONF_I2S_DIN_PIN = "i2s_din_pin" +CONF_I2S_MCLK_PIN = "i2s_mclk_pin" +CONF_I2S_BCLK_PIN = "i2s_bclk_pin" +CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" + +CONF_I2S_AUDIO = "i2s_audio" +CONF_I2S_AUDIO_ID = "i2s_audio_id" + +i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") +I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) +I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent)) +I2SAudioOut = i2s_audio_ns.class_( + "I2SAudioOut", cg.Parented.template(I2SAudioComponent) +) + +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +I2S_PORTS = { + VARIANT_ESP32: 2, + VARIANT_ESP32S2: 1, + VARIANT_ESP32S3: 2, + VARIANT_ESP32C3: 1, +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(I2SAudioComponent), + cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, + } +) + + +def _final_validate(_): + i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] + variant = get_esp32_variant() + if variant not in I2S_PORTS: + raise cv.Invalid(f"Unsupported variant {variant}") + if len(i2s_audio_configs) > I2S_PORTS[variant]: + raise cv.Invalid( + f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) + if CONF_I2S_BCLK_PIN in config: + cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) + if CONF_I2S_MCLK_PIN in config: + cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp new file mode 100644 index 000000000000..c1a608c064e0 --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -0,0 +1,30 @@ +#include "i2s_audio.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +namespace esphome { +namespace i2s_audio { + +static const char *const TAG = "i2s_audio"; + +void I2SAudioComponent::setup() { + static i2s_port_t next_port_num = I2S_NUM_0; + + if (next_port_num >= I2S_NUM_MAX) { + ESP_LOGE(TAG, "Too many I2S Audio components!"); + this->mark_failed(); + return; + } + + this->port_ = next_port_num; + next_port_num = (i2s_port_t) (next_port_num + 1); + + ESP_LOGCONFIG(TAG, "Setting up I2S Audio..."); +} + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h new file mode 100644 index 000000000000..d8d4a23dde9b --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -0,0 +1,57 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace i2s_audio { + +class I2SAudioComponent; + +class I2SAudioIn : public Parented {}; + +class I2SAudioOut : public Parented {}; + +class I2SAudioComponent : public Component { + public: + void setup() override; + + i2s_pin_config_t get_pin_config() const { + return { + .mck_io_num = this->mclk_pin_, + .bck_io_num = this->bclk_pin_, + .ws_io_num = this->lrclk_pin_, + .data_out_num = I2S_PIN_NO_CHANGE, + .data_in_num = I2S_PIN_NO_CHANGE, + }; + } + + void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } + void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } + void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } + + void lock() { this->lock_.lock(); } + bool try_lock() { return this->lock_.try_lock(); } + void unlock() { this->lock_.unlock(); } + + i2s_port_t get_port() const { return this->port_; } + + protected: + Mutex lock_; + + I2SAudioIn *audio_in_{nullptr}; + I2SAudioOut *audio_out_{nullptr}; + + int mclk_pin_{I2S_PIN_NO_CHANGE}; + int bclk_pin_{I2S_PIN_NO_CHANGE}; + int lrclk_pin_; + i2s_port_t port_{}; +}; + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/media_player.py b/esphome/components/i2s_audio/media_player/__init__.py similarity index 68% rename from esphome/components/i2s_audio/media_player.py rename to esphome/components/i2s_audio/media_player/__init__.py index 43a48a721ea4..600a308e6c2c 100644 --- a/esphome/components/i2s_audio/media_player.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -5,25 +5,29 @@ from esphome import pins from esphome.const import CONF_ID, CONF_MODE -from esphome.core import CORE -CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["esp32"] +from .. import ( + i2s_audio_ns, + I2SAudioComponent, + I2SAudioOut, + CONF_I2S_AUDIO_ID, + CONF_I2S_DOUT_PIN, +) -i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2s_audio"] I2SAudioMediaPlayer = i2s_audio_ns.class_( - "I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer + "I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer, I2SAudioOut ) i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") -CONF_I2S_DOUT_PIN = "i2s_dout_pin" -CONF_I2S_BCLK_PIN = "i2s_bclk_pin" -CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" + CONF_MUTE_PIN = "mute_pin" CONF_AUDIO_ID = "audio_id" CONF_DAC_TYPE = "dac_type" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" INTERNAL_DAC_OPTIONS = { "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, @@ -35,6 +39,8 @@ NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] +I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] + def validate_esp32_variant(config): if config[CONF_DAC_TYPE] != "internal": @@ -48,34 +54,29 @@ def validate_esp32_variant(config): CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "internal": cv.Schema( + "internal": media_player.MEDIA_PLAYER_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), } - ) - .extend(media_player.MEDIA_PLAYER_SCHEMA) - .extend(cv.COMPONENT_SCHEMA), - "external": cv.Schema( + ).extend(cv.COMPONENT_SCHEMA), + "external": media_player.MEDIA_PLAYER_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required( CONF_I2S_DOUT_PIN ): pins.internal_gpio_output_pin_number, - cv.Required( - CONF_I2S_BCLK_PIN - ): pins.internal_gpio_output_pin_number, - cv.Required( - CONF_I2S_LRCLK_PIN - ): pins.internal_gpio_output_pin_number, cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_MODE, default="mono"): cv.one_of( *EXTERNAL_DAC_OPTIONS, lower=True ), + cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( + *I2C_COMM_FMT_OPTIONS, lower=True + ), } - ) - .extend(media_player.MEDIA_PLAYER_SCHEMA) - .extend(cv.COMPONENT_SCHEMA), + ).extend(cv.COMPONENT_SCHEMA), }, key=CONF_DAC_TYPE, ), @@ -89,19 +90,19 @@ async def to_code(config): await cg.register_component(var, config) await media_player.register_media_player(var, config) + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + if config[CONF_DAC_TYPE] == "internal": cg.add(var.set_internal_dac_mode(config[CONF_MODE])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) - cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_MUTE_PIN in config: pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) cg.add(var.set_mute_pin(pin)) cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) - if CORE.is_esp32: - cg.add_library("WiFiClientSecure", None) - cg.add_library("HTTPClient", None) - cg.add_library("esphome/ESP32-audioI2S", "2.0.6") - cg.add_build_flag("-DAUDIO_NO_SD_FS") + cg.add_library("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) + cg.add_library("esphome/ESP32-audioI2S", "2.0.7") + cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/i2s_audio/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp similarity index 65% rename from esphome/components/i2s_audio/i2s_audio_media_player.cpp rename to esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 2b00a5ec26fe..9e2e3f136ab1 100644 --- a/esphome/components/i2s_audio/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -11,17 +11,25 @@ static const char *const TAG = "audio"; void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { if (call.get_media_url().has_value()) { - if (this->audio_->isRunning()) - this->audio_->stopSong(); - this->high_freq_.start(); - this->audio_->connecttohost(call.get_media_url().value().c_str()); - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + this->current_url_ = call.get_media_url(); + + if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && this->audio_ != nullptr) { + if (this->audio_->isRunning()) { + this->audio_->stopSong(); + } + this->audio_->connecttohost(this->current_url_.value().c_str()); + } else { + this->start(); + } } if (call.get_volume().has_value()) { this->volume = call.get_volume().value(); this->set_volume_(volume); this->unmute_(); } + if (this->i2s_state_ != I2S_STATE_RUNNING) { + return; + } if (call.get_command().has_value()) { switch (call.get_command().value()) { case media_player::MEDIA_PLAYER_COMMAND_PLAY: @@ -35,7 +43,7 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; break; case media_player::MEDIA_PLAYER_COMMAND_STOP: - this->stop_(); + this->stop(); break; case media_player::MEDIA_PLAYER_COMMAND_MUTE: this->mute_(); @@ -89,27 +97,58 @@ void I2SAudioMediaPlayer::unmute_() { this->muted_ = false; } void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { - this->audio_->setVolume(remap(volume, 0.0f, 1.0f, 0, 21)); + if (this->audio_ != nullptr) + this->audio_->setVolume(remap(volume, 0.0f, 1.0f, 0, 21)); if (publish) this->volume = volume; } -void I2SAudioMediaPlayer::stop_() { - if (this->audio_->isRunning()) - this->audio_->stopSong(); - this->high_freq_.stop(); +void I2SAudioMediaPlayer::setup() { + ESP_LOGCONFIG(TAG, "Setting up Audio..."); this->state = media_player::MEDIA_PLAYER_STATE_IDLE; } -void I2SAudioMediaPlayer::setup() { - ESP_LOGCONFIG(TAG, "Setting up Audio..."); +void I2SAudioMediaPlayer::loop() { + switch (this->i2s_state_) { + case I2S_STATE_STARTING: + this->start_(); + break; + case I2S_STATE_RUNNING: + this->play_(); + break; + case I2S_STATE_STOPPING: + this->stop_(); + break; + case I2S_STATE_STOPPED: + break; + } +} + +void I2SAudioMediaPlayer::play_() { + this->audio_->loop(); + if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) { + this->stop(); + } +} + +void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; } +void I2SAudioMediaPlayer::start_() { + if (!this->parent_->try_lock()) { + return; // Waiting for another i2s to return lock + } + #if SOC_I2S_SUPPORTS_DAC if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { - this->audio_ = make_unique