From 881cd535b9c61bffde6a183945dda139a0c3d8dc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Feb 2023 15:33:02 +1300 Subject: [PATCH 001/115] Bump version to 2023.3.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0c4c5a22e8..e5d0b0bb10 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.2.0-dev" +__version__ = "2023.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 9dd01b30bd4e3023b30b446e48eb0e844dc75f92 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Feb 2023 16:26:06 +1300 Subject: [PATCH 002/115] Bump curl version in docker (#4403) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 66b708f522..ddc666cf6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ RUN \ python3-cryptography=3.3.2-1 \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ - curl=7.74.0-1.3+deb11u3 \ + curl=7.74.0-1.3+deb11u5 \ openssh-client=1:8.4p1-5+deb11u1 \ && rm -rf \ /tmp/* \ From 4d192c7387028c0e9989bf5e8f726d0d42a08862 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Feb 2023 17:37:55 +1300 Subject: [PATCH 003/115] Fix release workflow (#4405) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 738bb5a6df..3904834dc9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -150,6 +150,6 @@ jobs: ref: "main", inputs: { version: "${{ github.event.release.tag_name }}", - content: "${{ toJSON(github.event.release.body) }}" + content: ${{ toJSON(github.event.release.body) }} } }) From 499cb615f10a7ed2ce814791b6e57206d9c87e2e Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 9 Feb 2023 06:17:45 +0100 Subject: [PATCH 004/115] socket: Format IPv4-mapped IPv6 addresses as regular IPv4 address (#4382) --- esphome/components/socket/bsd_sockets_impl.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 6636bcb3eb..b21341e4d6 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -18,17 +18,19 @@ std::string format_sockaddr(const struct sockaddr_storage &storage) { if (storage.ss_family == AF_INET) { const struct sockaddr_in *addr = reinterpret_cast(&storage); char buf[INET_ADDRSTRLEN]; - const char *ret = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; + if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) != nullptr) + return std::string{buf}; } else if (storage.ss_family == AF_INET6) { const struct sockaddr_in6 *addr = reinterpret_cast(&storage); char buf[INET6_ADDRSTRLEN]; - const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; + // Format IPv4-mapped IPv6 addresses as regular IPv4 addresses + if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && + addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && + inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf, sizeof(buf)) != nullptr) { + return std::string{buf}; + } + if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) != nullptr) + return std::string{buf}; } return {}; } From 2a8745d7e0b524ef22b34a75c7e559aee3ad02c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 18:27:30 +1300 Subject: [PATCH 005/115] Bump frenck/action-yamllint from 1.3.1 to 1.4.0 (#4289) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c32dc11b61..c28c8c485c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,7 +181,7 @@ jobs: - name: Run yamllint if: matrix.id == 'yamllint' - uses: frenck/action-yamllint@v1.3.1 + uses: frenck/action-yamllint@v1.4.0 - name: Suggested changes run: script/ci-suggest-changes From b14e774a27a01084647a39357a24cd347df53029 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 18:27:58 +1300 Subject: [PATCH 006/115] Bump pyupgrade from 3.3.0 to 3.3.1 (#4160) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5ae80da3b..7a954af4a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test.txt b/requirements_test.txt index 8404818c95..6f03f005d5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==2.15.10 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating black==22.12.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.3.0 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests From 045489e6d7c9b772509feda0f962be35d7b766b2 Mon Sep 17 00:00:00 2001 From: Patrick Collins Date: Thu, 9 Feb 2023 20:37:31 +1100 Subject: [PATCH 007/115] Climate PID Autotune Logging fixes (#4136) * pid autotune logging fixes * fixed clang-format request * improved and clarified logging * changed logging not to alter the TAG * logging now does not alter TAG. fixed clang formattting * fixed string issues * playing with strings to please the clang gods * playing with strings * Delete secrets.yaml * Delete console-fan-autotune-test.yaml * Update esphome/components/pid/pid_autotuner.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/pid/pid_autotuner.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/pid/pid_autotuner.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/pid/pid_autotuner.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/pid/pid_autotuner.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/pid/pid_autotuner.cpp | 28 +++++++++++++----------- esphome/components/pid/pid_autotuner.h | 3 +++ esphome/components/pid/pid_climate.cpp | 17 +++++++++++--- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index fc012aaa39..1b3ddebcc5 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -1,6 +1,10 @@ #include "pid_autotuner.h" #include "esphome/core/log.h" +#ifndef M_PI +#define M_PI 3.1415926535897932384626433 +#endif + namespace esphome { namespace pid { @@ -73,7 +77,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } if (!std::isnan(this->setpoint_) && this->setpoint_ != setpoint) { - ESP_LOGW(TAG, "Setpoint changed during autotune! The result will not be accurate!"); + ESP_LOGW(TAG, "%s: Setpoint changed during autotune! The result will not be accurate!", this->id_.c_str()); } this->setpoint_ = setpoint; @@ -87,7 +91,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce if (!this->frequency_detector_.has_enough_data() || !this->amplitude_detector_.has_enough_data()) { // not enough data for calculation yet - ESP_LOGV(TAG, " Not enough data yet for aututuner"); + ESP_LOGV(TAG, "%s: Not enough data yet for autotuner", this->id_.c_str()); return res; } @@ -97,12 +101,13 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway if (zc_symmetrical) { - ESP_LOGVV(TAG, " ZC is not symmetrical"); + ESP_LOGVV(TAG, "%s: ZC is not symmetrical", this->id_.c_str()); } if (amplitude_convergent) { - ESP_LOGVV(TAG, " Amplitude is not convergent"); + ESP_LOGVV(TAG, "%s: Amplitude is not convergent", this->id_.c_str()); } uint32_t phase = this->relay_function_.phase_count; + ESP_LOGVV(TAG, "%s: >", this->id_.c_str()); ESP_LOGVV(TAG, " Phase %u, enough=%u", phase, enough_data_phase_); if (this->enough_data_phase_ == 0) { @@ -116,7 +121,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } } - ESP_LOGI(TAG, "PID Autotune finished!"); + ESP_LOGI(TAG, "%s: PID Autotune finished!", this->id_.c_str()); float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude(); float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f; @@ -131,12 +136,12 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce return res; } void PIDAutotuner::dump_config() { - ESP_LOGI(TAG, "PID Autotune:"); if (this->state_ == AUTOTUNE_SUCCEEDED) { + ESP_LOGI(TAG, "%s: PID Autotune:", this->id_.c_str()); ESP_LOGI(TAG, " State: Succeeded!"); bool has_issue = false; if (!this->amplitude_detector_.is_amplitude_convergent()) { - ESP_LOGW(TAG, " Could not reliable determine oscillation amplitude, PID parameters may be inaccurate!"); + ESP_LOGW(TAG, " Could not reliably determine oscillation amplitude, PID parameters may be inaccurate!"); ESP_LOGW(TAG, " Please make sure you eliminate all outside influences on the measured temperature."); has_issue = true; } @@ -173,10 +178,12 @@ void PIDAutotuner::dump_config() { print_rule_("Pessen Integral PID", 0.7f, 1.75f, 0.105f); print_rule_("Some Overshoot PID", 0.333f, 0.667f, 0.111f); print_rule_("No Overshoot PID", 0.2f, 0.4f, 0.0625f); + ESP_LOGI(TAG, "%s: Autotune completed", this->id_.c_str()); } if (this->state_ == AUTOTUNE_RUNNING) { - ESP_LOGI(TAG, " Autotune is still running!"); + ESP_LOGD(TAG, "%s: PID Autotune:", this->id_.c_str()); + ESP_LOGD(TAG, " Autotune is still running!"); ESP_LOGD(TAG, " Status: Trying to reach %.2f °C", setpoint_ - relay_function_.current_target_error()); ESP_LOGD(TAG, " Stats so far:"); ESP_LOGD(TAG, " Phases: %u", relay_function_.phase_count); @@ -221,7 +228,6 @@ float PIDAutotuner::RelayFunction::update(float error) { float output = state == RELAY_FUNCTION_POSITIVE ? output_positive : output_negative; if (change) { this->phase_count++; - ESP_LOGV(TAG, "Autotune: Turning output to %.1f%%", output * 100); } return output; @@ -245,10 +251,8 @@ void PIDAutotuner::OscillationFrequencyDetector::update(uint32_t now, float erro if (had_crossing) { // Had crossing above hysteresis threshold, record - ESP_LOGV(TAG, "Autotune: Detected Zero-Cross at %u", now); if (this->last_zerocross != 0) { uint32_t dt = now - this->last_zerocross; - ESP_LOGV(TAG, " dt: %u", dt); this->zerocrossing_intervals.push_back(dt); } this->last_zerocross = now; @@ -297,13 +301,11 @@ void PIDAutotuner::OscillationAmplitudeDetector::update(float error, // The positive error peak must have been in previous segment (180° shifted) // record phase_max this->phase_maxs.push_back(phase_max); - ESP_LOGV(TAG, "Autotune: Phase Max: %f", phase_max); } else if (last_relay_state == RelayFunction::RELAY_FUNCTION_NEGATIVE) { // Transitioned from negative error to positive error. // The negative error peak must have been in previous segment (180° shifted) // record phase_min this->phase_mins.push_back(phase_min); - ESP_LOGV(TAG, "Autotune: Phase Min: %f", phase_min); } // reset phase values for next phase this->phase_min = error; diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h index 88716d2b89..98dc02bcc4 100644 --- a/esphome/components/pid/pid_autotuner.h +++ b/esphome/components/pid/pid_autotuner.h @@ -31,6 +31,8 @@ class PIDAutotuner { void dump_config(); + void set_autotuner_id(std::string id) { this->id_ = std::move(id); } + void set_noiseband(float noiseband) { relay_function_.noiseband = noiseband; // ZC detector uses 1/4 the noiseband of relay function (noise suppression) @@ -106,6 +108,7 @@ class PIDAutotuner { } state_ = AUTOTUNE_RUNNING; float ku_; float pu_; + std::string id_; }; } // namespace pid diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 81c3e1f12e..dab4502d40 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -130,9 +130,6 @@ void PIDClimate::update_pid_() { // keep autotuner instance so that subsequent dump_configs will print the long result message. } else { value = res.output; - if (mode != climate::CLIMATE_MODE_HEAT_COOL) { - ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!"); - } } } } @@ -151,10 +148,24 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { float min_value = this->supports_cool_() ? -1.0f : 0.0f; float max_value = this->supports_heat_() ? 1.0f : 0.0f; this->autotuner_->config(min_value, max_value); + this->autotuner_->set_autotuner_id(this->get_object_id()); + + ESP_LOGI(TAG, + "%s: Autotune has started. This can take a long time depending on the " + "responsiveness of your system. Your system " + "output will be altered to deliberately oscillate above and below the setpoint multiple times. " + "Until your sensor provides a reading, the autotuner may display \'nan\'", + this->get_object_id().c_str()); + this->set_interval("autotune-progress", 10000, [this]() { if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) this->autotuner_->dump_config(); }); + + if (mode != climate::CLIMATE_MODE_HEAT_COOL) { + ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!", + this->get_object_id().c_str()); + } } void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } From b7ab00b6994f1feae81df88a42e20e29c0737d29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Feb 2023 07:40:04 +1300 Subject: [PATCH 008/115] Bump black from 22.12.0 to 23.1.0 (#4375) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- esphome/components/b_parasite/sensor.py | 2 +- esphome/components/esp8266/__init__.py | 1 - esphome/components/honeywellabp/sensor.py | 1 - esphome/components/nextion/base_component.py | 1 - esphome/components/nextion/sensor/__init__.py | 1 - esphome/components/pmsa003i/sensor.py | 1 - esphome/components/script/__init__.py | 1 - esphome/components/sht4x/sensor.py | 1 - esphome/dashboard/dashboard.py | 1 - requirements_test.txt | 2 +- 11 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a954af4a6..0de82cf2de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/ambv/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index 201685adc4..1b65bf7f1d 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -80,7 +80,7 @@ async def to_code(config): cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) - for (config_key, setter) in [ + for config_key, setter in [ (CONF_TEMPERATURE, var.set_temperature), (CONF_HUMIDITY, var.set_humidity), (CONF_BATTERY_VOLTAGE, var.set_battery_voltage), diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 8715a3b4e6..59a1f2cd85 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -240,7 +240,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/honeywellabp/sensor.py b/esphome/components/honeywellabp/sensor.py index 720a96b93c..ed8bff6e9b 100644 --- a/esphome/components/honeywellabp/sensor.py +++ b/esphome/components/honeywellabp/sensor.py @@ -52,7 +52,6 @@ CONFIG_SCHEMA = ( 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/nextion/base_component.py b/esphome/components/nextion/base_component.py index 06216e9ce0..b2a857c888 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -92,7 +92,6 @@ CONFIG_SWITCH_COMPONENT_SCHEMA = CONFIG_SENSOR_COMPONENT_SCHEMA.extend( async def setup_component_core_(var, config, arg): - if CONF_VARIABLE_NAME in config: cg.add(var.set_variable_name(config[CONF_VARIABLE_NAME])) elif CONF_COMPONENT_NAME in config: diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index b022007ddd..eefbe34d58 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -69,7 +69,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - hub = await cg.get_variable(config[CONF_NEXTION_ID]) var = cg.new_Pvariable(config[CONF_ID], hub) await cg.register_component(var, config) diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py index ceca791cd6..ef620614a2 100644 --- a/esphome/components/pmsa003i/sensor.py +++ b/esphome/components/pmsa003i/sensor.py @@ -122,7 +122,6 @@ async def to_code(config): cg.add(var.set_standard_units(config[CONF_STANDARD_UNITS])) 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/script/__init__.py b/esphome/components/script/__init__.py index 907d7bf0e3..6337d89bcd 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -62,7 +62,6 @@ def assign_declare_id(value): def parameters_to_template(args): - template_args = [] func_args = [] script_arg_names = [] diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index 9fb8fc969e..e195bb9acc 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -98,7 +98,6 @@ async def to_code(config): cg.add(var.set_heater_duty_value(config[CONF_HEATER_MAX_DUTY])) 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/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 0ff5362670..6c7188a4ac 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -977,7 +977,6 @@ class LogoutHandler(BaseHandler): class SecretKeysRequestHandler(BaseHandler): @authenticated def get(self): - filename = None for secret_filename in const.SECRETS_FILES: diff --git a/requirements_test.txt b/requirements_test.txt index 6f03f005d5..a1c0845089 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==2.15.10 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating -black==22.12.0 # also change in .pre-commit-config.yaml when updating +black==23.1.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating pre-commit From f8acc45be4340075fd07f91c5a424c29500f00d8 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Fri, 10 Feb 2023 21:56:15 +0100 Subject: [PATCH 009/115] Add Clipping to displaybuffer (#4271) * adding Clipping support to the displaybuffer - add rect structure * removed unused define * add missing property for storing the clipped areas * include log header * Move Rect method's code to cpp file - removed obsolete remarks * fixed reported issues * make Rect class methods public * clang fix * Remove commented code * Renaming clipping methods * Multiple changes: - replaced 32766 with VALUE_NO_SET - fixed the way *_clipping(left, top, right, bottom) is stored - add `is_clipping();` - make sure that all clipped region are closed after `do_update_()` - rename de parameters for `Rect::expand();` * remove unneeded space * replace define with static const uint8_t * correcting my copy paste mistake --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .gitignore | 2 + esphome/components/display/display_buffer.cpp | 121 ++++++++++++++++++ esphome/components/display/display_buffer.h | 84 +++++++++++- 3 files changed, 206 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 110437c368..71b66b2499 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ tests/.esphome/ sdkconfig.* !sdkconfig.defaults + +.tests/ \ No newline at end of file diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index cfd73509ea..1b24ca94b9 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -15,6 +15,84 @@ static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); +void Rect::expand(int16_t horizontal, int16_t vertical) { + if ((*this).is_set() && ((*this).w >= (-2 * horizontal)) && ((*this).h >= (-2 * vertical))) { + (*this).x = (*this).x - horizontal; + (*this).y = (*this).y - vertical; + (*this).w = (*this).w + (2 * horizontal); + (*this).h = (*this).h + (2 * vertical); + } +} + +void Rect::extend(Rect rect) { + if (!this->is_set()) { + this->x = rect.x; + this->y = rect.y; + this->w = rect.w; + this->h = rect.h; + } else { + if (this->x > rect.x) { + this->x = rect.x; + } + if (this->y > rect.y) { + this->y = rect.y; + } + if (this->x2() < rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() < rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} +void Rect::shrink(Rect rect) { + if (!this->inside(rect)) { + (*this) = Rect(); + } else { + if (this->x < rect.x) { + this->x = rect.x; + } + if (this->y < rect.y) { + this->y = rect.y; + } + if (this->x2() > rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() > rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} + +bool Rect::inside(int16_t x, int16_t y, bool absolute) { // NOLINT + if (!this->is_set()) { + return true; + } + if (absolute) { + return ((x >= 0) && (x <= this->w) && (y >= 0) && (y <= this->h)); + } else { + return ((x >= this->x) && (x <= this->x2()) && (y >= this->y) && (y <= this->y2())); + } +} + +bool Rect::inside(Rect rect, bool absolute) { + if (!this->is_set() || !rect.is_set()) { + return true; + } + if (absolute) { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); + } else { + return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } +} + +void Rect::info(const std::string &prefix) { + if (this->is_set()) { + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d]", prefix.c_str(), this->x, this->y, this->w, this->h); + } else + ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); +} + void DisplayBuffer::init_internal_(uint32_t buffer_length) { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->buffer_ = allocator.allocate(buffer_length); @@ -24,6 +102,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } + void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { @@ -50,6 +129,9 @@ int DisplayBuffer::get_height() { } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; // NOLINT + switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -368,6 +450,10 @@ void DisplayBuffer::do_update_() { } else if (this->writer_.has_value()) { (*this->writer_)(*this); } + // remove all not ended clipping regions + while (is_clipping()) { + end_clipping(); + } } void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) @@ -392,6 +478,41 @@ void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time: } #endif +void DisplayBuffer::start_clipping(Rect rect) { + if (!this->clipping_rectangle_.empty()) { + Rect r = this->clipping_rectangle_.back(); + rect.shrink(r); + } + this->clipping_rectangle_.push_back(rect); +} +void DisplayBuffer::end_clipping() { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "clear: Clipping is not set."); + } else { + this->clipping_rectangle_.pop_back(); + } +} +void DisplayBuffer::extend_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().extend(add_rect); + } +} +void DisplayBuffer::shrink_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().shrink(add_rect); + } +} +Rect DisplayBuffer::get_clipping() { + if (this->clipping_rectangle_.empty()) { + return Rect(); + } else { + return this->clipping_rectangle_.back(); + } +} bool Glyph::get_pixel(int x, int y) const { const int x_data = x - this->glyph_data_->offset_x; const int y_data = y - this->glyph_data_->offset_y; diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b652989067..03d64f80d3 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -4,7 +4,6 @@ #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "display_color_utils.h" - #include #include @@ -100,6 +99,32 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; +static const int16_t VALUE_NO_SET = 32766; + +class Rect { + public: + int16_t x; ///< X coordinate of corner + int16_t y; ///< Y coordinate of corner + int16_t w; ///< Width of region + int16_t h; ///< Height of region + + Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT + inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} + inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner + inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner + + inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } + + void expand(int16_t horizontal, int16_t vertical); + + void extend(Rect rect); + void shrink(Rect rect); + + bool inside(Rect rect, bool absolute = false); + bool inside(int16_t x, int16_t y, bool absolute = false); + void info(const std::string &prefix = "rect info:"); +}; + class Font; class Image; class DisplayBuffer; @@ -126,6 +151,7 @@ class DisplayBuffer { int get_width(); /// Get the height of the image in pixels with rotation applied. int get_height(); + /// Set a single pixel at the specified coordinates to the given color. void draw_pixel_at(int x, int y, Color color = COLOR_ON); @@ -374,6 +400,61 @@ class DisplayBuffer { */ virtual DisplayType get_display_type() = 0; + /// + /// Set the clipping rectangle for further drawing + /// + /// \param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + /// + /// \return true if success, false if error + /// + void start_clipping(Rect rect); + void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + start_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /// + /// Add a rectangular region to the invalidation region + /// - This is usually called when an element has been modified + /// + /// \param[in] rect: Rectangle to add to the invalidation region + /// + /// \return none + /// + void extend_clipping(Rect rect); + void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + this->extend_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /// + /// substract a rectangular region to the invalidation region + /// - This is usually called when an element has been modified + /// + /// \param[in] rect: Rectangle to add to the invalidation region + /// + /// \return none + /// + void shrink_clipping(Rect rect); + void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + this->shrink_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /// + /// Reset the invalidation region + /// + /// \return none + /// + void end_clipping(); + + /// + /// Get the current the clipping rectangle + /// + /// + /// \return rect for active clipping region + /// + Rect get_clipping(); + + bool is_clipping() const { return this->clipping_rectangle_.empty(); } + protected: void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); @@ -390,6 +471,7 @@ class DisplayBuffer { DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; bool auto_clear_enabled_{true}; + std::vector clipping_rectangle_; }; class DisplayPage { From 58eeb6b1b8362c1811510988545553e3544731a5 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Sun, 12 Feb 2023 17:03:53 +0100 Subject: [PATCH 010/115] Fix check for empty clipping array (#4421) --- esphome/components/display/display_buffer.cpp | 10 ++-- esphome/components/display/display_buffer.h | 58 ++++++++----------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 1b24ca94b9..9fe4137a14 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -16,11 +16,11 @@ const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); void Rect::expand(int16_t horizontal, int16_t vertical) { - if ((*this).is_set() && ((*this).w >= (-2 * horizontal)) && ((*this).h >= (-2 * vertical))) { - (*this).x = (*this).x - horizontal; - (*this).y = (*this).y - vertical; - (*this).w = (*this).w + (2 * horizontal); - (*this).h = (*this).h + (2 * vertical); + if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { + this->x = this->x - horizontal; + this->y = this->y - vertical; + this->w = this->w + (2 * horizontal); + this->h = this->h + (2 * vertical); } } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 03d64f80d3..3763da8041 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -400,60 +400,48 @@ class DisplayBuffer { */ virtual DisplayType get_display_type() = 0; - /// - /// Set the clipping rectangle for further drawing - /// - /// \param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) - /// - /// \return true if success, false if error - /// + /** Set the clipping rectangle for further drawing + * + * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + * + * return true if success, false if error + */ void start_clipping(Rect rect); void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { start_clipping(Rect(left, top, right - left, bottom - top)); }; - /// - /// Add a rectangular region to the invalidation region - /// - This is usually called when an element has been modified - /// - /// \param[in] rect: Rectangle to add to the invalidation region - /// - /// \return none - /// + /** Add a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ void extend_clipping(Rect rect); void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { this->extend_clipping(Rect(left, top, right - left, bottom - top)); }; - /// - /// substract a rectangular region to the invalidation region - /// - This is usually called when an element has been modified - /// - /// \param[in] rect: Rectangle to add to the invalidation region - /// - /// \return none - /// + /** substract a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ void shrink_clipping(Rect rect); void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { this->shrink_clipping(Rect(left, top, right - left, bottom - top)); }; - /// - /// Reset the invalidation region - /// - /// \return none - /// + /** Reset the invalidation region + */ void end_clipping(); - /// - /// Get the current the clipping rectangle - /// - /// - /// \return rect for active clipping region - /// + /** Get the current the clipping rectangle + * + * return rect for active clipping region + */ Rect get_clipping(); - bool is_clipping() const { return this->clipping_rectangle_.empty(); } + bool is_clipping() const { return !this->clipping_rectangle_.empty(); } protected: void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); From b8c0f884404f3c5950d36d8715050186948e37fc Mon Sep 17 00:00:00 2001 From: Andre Borie Date: Mon, 13 Feb 2023 01:14:35 +0000 Subject: [PATCH 011/115] Improve tuya network status command (#4415) --- esphome/components/tuya/tuya.cpp | 29 +++++++++++++++++++++++++++-- esphome/components/tuya/tuya.h | 2 ++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 89a687e5a6..efe214f6a3 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -5,6 +5,10 @@ #include "esphome/core/util.h" #include "esphome/core/gpio.h" +#ifdef USE_CAPTIVE_PORTAL +#include "esphome/components/captive_portal/captive_portal.h" +#endif + namespace esphome { namespace tuya { @@ -243,6 +247,14 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); #endif break; + case TuyaCommandType::GET_NETWORK_STATUS: { + uint8_t wifi_status = this->get_wifi_status_code_(); + + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::GET_NETWORK_STATUS, .payload = std::vector{wifi_status}}); + ESP_LOGV(TAG, "Network status requested, reported as %i", wifi_status); + break; + } default: ESP_LOGE(TAG, "Invalid command (0x%02X) received", command); } @@ -437,8 +449,9 @@ void Tuya::set_status_pin_() { this->status_pin_.value()->digital_write(is_network_ready); } -void Tuya::send_wifi_status_() { +uint8_t Tuya::get_wifi_status_code_() { uint8_t status = 0x02; + if (network::is_connected()) { status = 0x03; @@ -446,7 +459,19 @@ void Tuya::send_wifi_status_() { if (this->protocol_version_ >= 0x03 && remote_is_connected()) { status = 0x04; } - } + } else { +#ifdef USE_CAPTIVE_PORTAL + if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) { + status = 0x01; + } +#endif + }; + + return status; +} + +void Tuya::send_wifi_status_() { + uint8_t status = this->get_wifi_status_code_(); if (status == this->wifi_status_) { return; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 5839dfbec0..cc13a64bdb 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -55,6 +55,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, + GET_NETWORK_STATUS = 0x2B, }; enum class TuyaInitState : uint8_t { @@ -120,6 +121,7 @@ class Tuya : public Component, public uart::UARTDevice { void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); void set_status_pin_(); void send_wifi_status_(); + uint8_t get_wifi_status_code_(); #ifdef USE_TIME void send_local_time_(); From 78f5c417a42386814934648459fc370a82b06f47 Mon Sep 17 00:00:00 2001 From: Andre Borie Date: Mon, 13 Feb 2023 01:43:11 +0000 Subject: [PATCH 012/115] Gracefully reject vacuum map upload requests (#4414) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/tuya/tuya.cpp | 5 +++++ esphome/components/tuya/tuya.h | 1 + 2 files changed, 6 insertions(+) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index efe214f6a3..fad4bb0bac 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -247,6 +247,11 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); #endif break; + case TuyaCommandType::VACUUM_MAP_UPLOAD: + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::VACUUM_MAP_UPLOAD, .payload = std::vector{0x01}}); + ESP_LOGW(TAG, "Vacuum map upload requested, responding that it is not enabled."); + break; case TuyaCommandType::GET_NETWORK_STATUS: { uint8_t wifi_status = this->get_wifi_status_code_(); diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index cc13a64bdb..b9c917f672 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -55,6 +55,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, + VACUUM_MAP_UPLOAD = 0x28, GET_NETWORK_STATUS = 0x2B, }; From 7a2d7fdd19093c2170cbafdda7427c8a869f0dd7 Mon Sep 17 00:00:00 2001 From: Expaso Date: Mon, 13 Feb 2023 03:43:52 +0100 Subject: [PATCH 013/115] Fixed PlatformIO Build on DEV (#4422) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/bedjet/bedjet_hub.cpp | 4 ++++ esphome/components/bedjet/bedjet_hub.h | 3 +-- esphome/components/ble_client/automation.cpp | 4 ++++ esphome/components/ble_client/automation.h | 4 ++-- esphome/components/esp32/post_build.py.script | 24 +++++++++++++------ esphome/components/mqtt/mqtt_light.cpp | 2 ++ esphome/core/defines.h | 2 +- platformio.ini | 1 + 8 files changed, 32 insertions(+), 12 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index f90ca5cf54..fbd2876dc9 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ESP32 + #include "bedjet_hub.h" #include "bedjet_child.h" #include "bedjet_const.h" @@ -541,3 +543,5 @@ void BedJetHub::register_child(BedJetClient *obj) { } // namespace bedjet } // namespace esphome + +#endif diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index fb757dfdf8..e4af2bca51 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -1,4 +1,5 @@ #pragma once +#ifdef USE_ESP32 #include "esphome/components/ble_client/ble_client.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -14,8 +15,6 @@ #include "esphome/components/time/real_time_clock.h" #endif -#ifdef USE_ESP32 - #include namespace esphome { diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 395950dd26..429f906a5f 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -1,3 +1,5 @@ +#ifdef USE_ESP32 + #include "automation.h" #include @@ -73,3 +75,5 @@ void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga } // namespace ble_client } // namespace esphome + +#endif diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index ef38333698..45ddba9782 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -1,13 +1,13 @@ #pragma once +#ifdef USE_ESP32 + #include #include #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" -#ifdef USE_ESP32 - namespace esphome { namespace ble_client { class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 406516a102..c941bdb386 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,15 +1,25 @@ # Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 -import os -if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: - import esptool -else: - import subprocess -from SCons.Script import ARGUMENTS - # pylint: disable=E0602 Import("env") # noqa +import os +import shutil + +if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + try: + import esptool + except ImportError: + env.Execute("$PYTHONEXE -m pip install esptool") +else: + import subprocess +from SCons.Script import ARGUMENTS + +# Copy over the default sdkconfig. +from os import path +if path.exists("./sdkconfig.defaults"): + os.makedirs(".temp", exist_ok=True) + shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") def esp32_create_combined_bin(source, target, env): verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index e2480acd62..f970da7d8c 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -3,6 +3,7 @@ #include "mqtt_const.h" +#ifdef USE_JSON #ifdef USE_MQTT #ifdef USE_LIGHT @@ -83,3 +84,4 @@ void MQTTJSONLightComponent::dump_config() { #endif #endif // USE_MQTT +#endif // USE_JSON diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 01fc16cc02..c3738c0365 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -25,6 +25,7 @@ #define USE_FAN #define USE_GRAPH #define USE_HOMEASSISTANT_TIME +#define USE_JSON #define USE_LIGHT #define USE_LOCK #define USE_LOGGER @@ -50,7 +51,6 @@ // Arduino-specific feature flags #ifdef USE_ARDUINO #define USE_CAPTIVE_PORTAL -#define USE_JSON #define USE_NEXTION_TFT_UPLOAD #define USE_PROMETHEUS #define USE_WEBSERVER diff --git a/platformio.ini b/platformio.ini index b92a1407ed..ef5820c4ad 100644 --- a/platformio.ini +++ b/platformio.ini @@ -185,6 +185,7 @@ build_flags = [env:esp32-arduino] extends = common:esp32-arduino board = esp32dev +board_build.partitions = huge_app.csv build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} From 3a101e8ec54524eb5f7859b6e14641f54eee910c Mon Sep 17 00:00:00 2001 From: WitchKing <31471327+xvil@users.noreply.github.com> Date: Mon, 13 Feb 2023 03:53:40 +0100 Subject: [PATCH 014/115] Ledc fix (#4338) --- esphome/components/ledc/ledc_output.cpp | 140 +++++++++++++++++++----- 1 file changed, 115 insertions(+), 25 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 34cf4cd1f6..d6241dd5b0 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -8,6 +8,20 @@ #endif #include +#define CLOCK_FREQUENCY 80e6f + +#ifdef USE_ARDUINO +#ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK +#undef CLOCK_FREQUENCY +// starting with ESP32 Arduino 2.0.2, the 40MHz crystal is used as clock by default if supported +#define CLOCK_FREQUENCY 40e6f +#endif +#else +#define DEFAULT_CLK LEDC_USE_APB_CLK +#endif + +static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; + namespace esphome { namespace ledc { @@ -26,11 +40,11 @@ inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif #endif -float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); } +float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return CLOCK_FREQUENCY / float(1 << bit_depth); } float ledc_min_frequency_for_bit_depth(uint8_t bit_depth, bool low_frequency) { const float max_div_num = ((1 << MAX_RES_BITS) - 1) / (low_frequency ? 32.0f : 256.0f); - return 80e6f / (max_div_num * float(1 << bit_depth)); + return CLOCK_FREQUENCY / (max_div_num * float(1 << bit_depth)); } optional ledc_bit_depth_for_frequency(float frequency) { @@ -46,6 +60,38 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } +#ifdef USE_ESP_IDF +esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, + uint8_t channel, uint8_t &bit_depth, float frequency) { + bit_depth = *ledc_bit_depth_for_frequency(frequency); + if (bit_depth < 1) { + ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", frequency); + } + + ledc_timer_config_t timer_conf{}; + timer_conf.speed_mode = speed_mode; + timer_conf.duty_resolution = static_cast(bit_depth); + timer_conf.timer_num = timer_num; + timer_conf.freq_hz = (uint32_t) frequency; + timer_conf.clk_cfg = DEFAULT_CLK; + + // Configure the time with fallback in case of error + int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; + esp_err_t init_result = ESP_FAIL; + while (attempt_count_max > 0 && init_result != ESP_OK) { + init_result = ledc_timer_config(&timer_conf); + if (init_result != ESP_OK) { + ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth); + // try again with a lower bit depth + timer_conf.duty_resolution = static_cast(--bit_depth); + } + attempt_count_max--; + } + + return init_result; +} +#endif + void LEDCOutput::write_state(float state) { if (!initialized_) { ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); @@ -61,6 +107,7 @@ void LEDCOutput::write_state(float state) { auto duty = static_cast(duty_rounded); #ifdef USE_ARDUINO + ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_); ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF @@ -72,6 +119,7 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { + ESP_LOGV(TAG, "Entering setup..."); #ifdef USE_ARDUINO this->update_frequency(this->frequency_); this->turn_off(); @@ -83,19 +131,16 @@ void LEDCOutput::setup() { auto timer_num = static_cast((channel_ % 8) / 2); auto chan_num = static_cast(channel_ % 8); - bit_depth_ = *ledc_bit_depth_for_frequency(frequency_); - if (bit_depth_ < 1) { - ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency_); - this->status_set_warning(); + esp_err_t timer_init_result = + configure_timer_frequency(speed_mode, timer_num, chan_num, this->channel_, this->bit_depth_, this->frequency_); + + if (timer_init_result != ESP_OK) { + ESP_LOGE(TAG, "Frequency %f can't be achieved with computed bit depth %u", this->frequency_, this->bit_depth_); + this->status_set_error(); + return; } - ledc_timer_config_t timer_conf{}; - timer_conf.speed_mode = speed_mode; - timer_conf.duty_resolution = static_cast(bit_depth_); - timer_conf.timer_num = timer_num; - timer_conf.freq_hz = (uint32_t) frequency_; - timer_conf.clk_cfg = LEDC_AUTO_CLK; - ledc_timer_config(&timer_conf); + ESP_LOGV(TAG, "Configured frequency %f with a bit depth of %u bits", this->frequency_, this->bit_depth_); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = pin_->get_pin(); @@ -107,6 +152,7 @@ void LEDCOutput::setup() { chan_conf.hpoint = 0; ledc_channel_config(&chan_conf); initialized_ = true; + this->status_clear_error(); #endif } @@ -114,36 +160,80 @@ void LEDCOutput::dump_config() { ESP_LOGCONFIG(TAG, "LEDC Output:"); LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " PWM Frequency: %.1f Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " Bit depth: %u", this->bit_depth_); + ESP_LOGV(TAG, " Max frequency for bit depth: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_)); + ESP_LOGV(TAG, " Min frequency for bit depth: %f", + ledc_min_frequency_for_bit_depth(this->bit_depth_, (this->frequency_ < 100))); + ESP_LOGV(TAG, " Max frequency for bit depth-1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ - 1)); + ESP_LOGV(TAG, " Min frequency for bit depth-1: %f", + ledc_min_frequency_for_bit_depth(this->bit_depth_ - 1, (this->frequency_ < 100))); + ESP_LOGV(TAG, " Max frequency for bit depth+1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ + 1)); + ESP_LOGV(TAG, " Min frequency for bit depth+1: %f", + ledc_min_frequency_for_bit_depth(this->bit_depth_ + 1, (this->frequency_ < 100))); + ESP_LOGV(TAG, " Max res bits: %d", MAX_RES_BITS); + ESP_LOGV(TAG, " Clock frequency: %f", CLOCK_FREQUENCY); } void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { - ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); - this->status_set_warning(); + ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", this->frequency_); + this->status_set_error(); } this->bit_depth_ = bit_depth_opt.value_or(8); this->frequency_ = frequency; #ifdef USE_ARDUINO - ledcSetup(this->channel_, frequency, this->bit_depth_); - initialized_ = true; + ESP_LOGV(TAG, "Using Arduino API - Trying to define channel, frequency and bit depth..."); + u_int32_t configured_frequency = 0; + + // Configure LEDC channel, frequency and bit depth with fallback + int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; + while (attempt_count_max > 0 && configured_frequency == 0) { + ESP_LOGV(TAG, "Trying initialize channel %u with frequency %.1f and bit depth of %u...", this->channel_, + this->frequency_, this->bit_depth_); + configured_frequency = ledcSetup(this->channel_, frequency, this->bit_depth_); + if (configured_frequency != 0) { + initialized_ = true; + this->status_clear_error(); + ESP_LOGV(TAG, "Configured frequency: %u with bit depth: %u", configured_frequency, this->bit_depth_); + } else { + ESP_LOGW(TAG, "Unable to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, + this->frequency_, this->bit_depth_); + // try again with a lower bit depth + this->bit_depth_--; + } + attempt_count_max--; + } + + if (configured_frequency == 0) { + ESP_LOGE(TAG, "Permanently failed to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, + this->frequency_, this->bit_depth_); + this->status_set_error(); + return; + } + #endif // USE_ARDUINO #ifdef USE_ESP_IDF if (!initialized_) { ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); return; } + auto speed_mode = get_speed_mode(channel_); auto timer_num = static_cast((channel_ % 8) / 2); + auto chan_num = static_cast(channel_ % 8); - ledc_timer_config_t timer_conf{}; - timer_conf.speed_mode = speed_mode; - timer_conf.duty_resolution = static_cast(bit_depth_); - timer_conf.timer_num = timer_num; - timer_conf.freq_hz = (uint32_t) frequency_; - timer_conf.clk_cfg = LEDC_AUTO_CLK; - ledc_timer_config(&timer_conf); + esp_err_t timer_init_result = + configure_timer_frequency(speed_mode, timer_num, chan_num, this->channel_, this->bit_depth_, this->frequency_); + + if (timer_init_result != ESP_OK) { + ESP_LOGE(TAG, "Frequency %f can't be achieved with computed bit depth %u", this->frequency_, this->bit_depth_); + this->status_set_error(); + return; + } + + this->status_clear_error(); #endif // re-apply duty this->write_state(this->duty_); From dfeeccfcca2ad452535cc9864d63f98e6dcf1b1c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:38:05 +1300 Subject: [PATCH 015/115] Add version api endpoint (#4429) --- esphome/dashboard/dashboard.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 6c7188a4ac..7fffdee00c 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -545,6 +545,14 @@ class DownloadBinaryRequestHandler(BaseHandler): self.finish() +class EsphomeVersionHandler(BaseHandler): + @authenticated + def get(self): + self.set_header("Content-Type", "application/json") + self.write(json.dumps({"version": const.__version__})) + self.finish() + + def _list_dashboard_entries(): files = settings.list_yaml_files() return [DashboardEntry(file) for file in files] @@ -1134,6 +1142,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}rename", EsphomeRenameHandler), (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), + (f"{rel}version", EsphomeVersionHandler), ], **app_settings, ) From 4f4ca61ada3f023a103768508ae621cf468f3fe7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:38:21 +1300 Subject: [PATCH 016/115] Handle uart.write in json-config endpoint (#4430) --- esphome/dashboard/dashboard.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 7fffdee00c..0bc1b0886a 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1007,8 +1007,14 @@ class SafeLoaderIgnoreUnknown(yaml.SafeLoader): def ignore_unknown(self, node): return f"{node.tag} {node.value}" + def construct_yaml_binary(self, node) -> str: + return super().construct_yaml_binary(node).decode("ascii") + SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) +SafeLoaderIgnoreUnknown.add_constructor( + "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary +) class JsonConfigRequestHandler(BaseHandler): From 458d6e24fc1c71346a2f99e37376f802796c9b89 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:38:37 +1300 Subject: [PATCH 017/115] Bump esphome-dashboard to 20230214.0 (#4431) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d59e0a1d2..9f4d34528c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==6.1.5 # When updating platformio, also update Dockerfile esptool==4.4 click==8.1.3 -esphome-dashboard==20230120.0 +esphome-dashboard==20230214.0 aioesphomeapi==13.1.0 zeroconf==0.47.1 From b4068dac565dcacfccf914a8a3831595e81582a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:41:14 +1300 Subject: [PATCH 018/115] Bump platformio from 6.1.5 to 6.1.6 (#4341) * Bump platformio from 6.1.5 to 6.1.6 Bumps [platformio](https://github.com/platformio/platformio) from 6.1.5 to 6.1.6. - [Release notes](https://github.com/platformio/platformio/releases) - [Changelog](https://github.com/platformio/platformio-core/blob/develop/HISTORY.rst) - [Commits](https://github.com/platformio/platformio/compare/v6.1.5...v6.1.6) --- updated-dependencies: - dependency-name: platformio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update dockerfile --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ddc666cf6a..dc9c4b7101 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -51,7 +51,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.37.1 \ - platformio==6.1.5 \ + platformio==6.1.6 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ diff --git a/requirements.txt b/requirements.txt index 9f4d34528c..f59f458e00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ tornado==6.2 tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.5 # When updating platformio, also update Dockerfile +platformio==6.1.6 # When updating platformio, also update Dockerfile esptool==4.4 click==8.1.3 esphome-dashboard==20230214.0 From 5997401e9e9ab19e24ae6bba33ef6bd8670b4df0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:55:36 +1300 Subject: [PATCH 019/115] Bump pylint from 2.15.10 to 2.16.2 (#4426) * Bump pylint from 2.15.10 to 2.16.2 Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.10 to 2.16.2. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.10...v2.16.2) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Lint --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/cpp_generator.py | 7 +++---- esphome/dashboard/dashboard.py | 2 +- esphome/loader.py | 8 ++++---- requirements_test.txt | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a423554d10..789bd58e5c 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -174,10 +174,9 @@ class ArrayInitializer(Expression): if not self.args: return "{}" if self.multiline: - cpp = "{\n" - for arg in self.args: - cpp += f" {arg},\n" - cpp += "}" + cpp = "{\n " + cpp += ",\n ".join(str(arg) for arg in self.args) + cpp += ",\n}" else: cpp = f"{{{', '.join(str(arg) for arg in self.args)}}}" return cpp diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 0bc1b0886a..1a50592a2d 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -753,7 +753,7 @@ class BoardsRequestHandler(BaseHandler): platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} # sort by board title boards_items = sorted(platform_boards.items(), key=lambda item: item[1]) - output = [dict(items=dict(boards_items))] + output = [{"items": dict(boards_items)}] self.set_header("content-type", "application/json") self.write(json.dumps(output)) diff --git a/esphome/loader.py b/esphome/loader.py index a0676eb90e..b245fa1610 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -167,10 +167,10 @@ def _lookup_module(domain): except Exception: # pylint: disable=broad-except _LOGGER.error("Unable to load component %s:", domain, exc_info=True) return None - else: - manif = ComponentManifest(module) - _COMPONENT_CACHE[domain] = manif - return manif + + manif = ComponentManifest(module) + _COMPONENT_CACHE[domain] = manif + return manif def get_component(domain): diff --git a/requirements_test.txt b/requirements_test.txt index a1c0845089..1e63f79b5e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.15.10 +pylint==2.16.2 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating black==23.1.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating From 025cf6320fc291b2bb9c942aa1b95dcdbe39054d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:56:14 +1300 Subject: [PATCH 020/115] Bump aioesphomeapi from 13.1.0 to 13.3.1 (#4427) Bumps [aioesphomeapi](https://github.com/esphome/aioesphomeapi) from 13.1.0 to 13.3.1. - [Release notes](https://github.com/esphome/aioesphomeapi/releases) - [Commits](https://github.com/esphome/aioesphomeapi/compare/v13.1.0...v13.3.1) --- updated-dependencies: - dependency-name: aioesphomeapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f59f458e00..00295af2b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.6 # When updating platformio, also update Dockerfile esptool==4.4 click==8.1.3 esphome-dashboard==20230214.0 -aioesphomeapi==13.1.0 +aioesphomeapi==13.3.1 zeroconf==0.47.1 # esp-idf requires this, but doesn't bundle it by default From 0d52f555b28570fee356332947e71234568f0dad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:07:09 +1300 Subject: [PATCH 021/115] Add concurrency limit to ci-docker (#4407) --- .github/workflows/ci-docker.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 86035b9259..1d2a1b5323 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -23,6 +23,11 @@ permissions: contents: read packages: read +concurrency: + # yamllint disable-line rule:line-length + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: check-docker: name: Build docker containers From c02871fdfefe683de3d2a9e234b6e79be503aa29 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Feb 2023 12:54:38 +1300 Subject: [PATCH 022/115] Add final job so branch protection can require matrix ci steps (#4432) --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28c8c485c..782fc5cb32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,3 +187,16 @@ jobs: run: script/ci-suggest-changes # yamllint disable-line rule:line-length if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') + + ci-status: + name: CI Status + runs-on: ubuntu-latest + needs: [ci] + if: always() + steps: + - name: Successful deploy + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: exit 0 + - name: Failing deploy + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 From a0d04ba091c76bd09d4d4be0a6a97cf6f86ff8df Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Tue, 14 Feb 2023 04:34:50 +0400 Subject: [PATCH 023/115] Fix setting wrong traits on midea climate component (#4425) * Fix issue3914 * Remove also default presets and modes * Fix traits after autoconf --- esphome/components/midea/air_conditioner.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 1ad5ade53d..b5bf43b64f 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -84,18 +84,18 @@ ClimateTraits AirConditioner::traits() { traits.set_supported_custom_presets(this->supported_custom_presets_); traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); /* + MINIMAL SET OF CAPABILITIES */ - traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); - traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); - traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); - traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); - traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); - traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) Converters::to_climate_traits(traits, this->base_.getCapabilities()); + if (!traits.get_supported_modes().empty()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); + if (!traits.get_supported_swing_modes().empty()) + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); + if (!traits.get_supported_presets().empty()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); return traits; } From 8b5b9e508b63313410151735e115dd260b0a6f03 Mon Sep 17 00:00:00 2001 From: Dominik Wagenknecht Date: Tue, 14 Feb 2023 22:09:07 +0100 Subject: [PATCH 024/115] Deep Sleep capable ports for ESP32S3 (#4230) Update to provide RTC capable ports for ESP32S3. Fresh from [espressif gpio docs](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/gpio.html) --- esphome/components/deep_sleep/__init__.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 8b60b4eb5f..bbd10d58c5 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -21,6 +21,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, + VARIANT_ESP32S3, ) WAKEUP_PINS = { @@ -69,6 +70,30 @@ WAKEUP_PINS = { 20, 21, ], + VARIANT_ESP32S3: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + ], } From 5307dfee2159026bf8b3ec483d6a5717087e8236 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 14 Feb 2023 23:45:27 +0100 Subject: [PATCH 025/115] Initialize all fields in ESPTime in PCF85063 (#4439) --- esphome/components/pcf85063/pcf85063.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index c6a8624ca7..5073522655 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -37,14 +37,18 @@ void PCF85063Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{.second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10), - .minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10), - .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), - .day_of_week = uint8_t(pcf85063_.reg.weekday), - .day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) - .month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10), - .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000)}; + time::ESPTime rtc_time{ + .second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10), + .minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10), + .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), + .day_of_week = uint8_t(pcf85063_.reg.weekday), + .day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10), + .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0, // overwritten by recalc_timestamp_utc(false) + }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); From 30eec5adee147c7926b5a553814db5f9b70a6d48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 19:19:03 +1300 Subject: [PATCH 026/115] Bump zeroconf from 0.47.1 to 0.47.3 (#4437) Bumps [zeroconf](https://github.com/python-zeroconf/python-zeroconf) from 0.47.1 to 0.47.3. - [Release notes](https://github.com/python-zeroconf/python-zeroconf/releases) - [Changelog](https://github.com/python-zeroconf/python-zeroconf/blob/master/CHANGELOG.md) - [Commits](https://github.com/python-zeroconf/python-zeroconf/compare/0.47.1...0.47.3) --- updated-dependencies: - dependency-name: zeroconf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 00295af2b9..c2490f36a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.4 click==8.1.3 esphome-dashboard==20230214.0 aioesphomeapi==13.3.1 -zeroconf==0.47.1 +zeroconf==0.47.3 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From dbe5587806f742baca223a117b056996686a309c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Feb 2023 13:27:32 +1300 Subject: [PATCH 027/115] Add ESPHome version to generated platformio.ini (#4443) * Add ESPHome version to generated platformio.ini * Move description to platformio section --- esphome/writer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/writer.py b/esphome/writer.py index 7a3c13e80b..2bf665c2b2 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -142,7 +142,10 @@ def get_ini_content(): # Sort to avoid changing build flags order CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) - content = f"[env:{CORE.name}]\n" + content = "[platformio]\n" + content += f"description = ESPHome {__version__}\n" + + content += f"[env:{CORE.name}]\n" content += format_ini(CORE.platformio_options) return content From 9aed758d1badf76270f1a81d1a5a62c78a41659f Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 16 Feb 2023 01:28:12 +0100 Subject: [PATCH 028/115] Automate syncing device classes with HA (#4438) * Sync device classes with HA * Rename blacklist --- esphome/components/binary_sensor/__init__.py | 4 +- esphome/components/button/__init__.py | 2 + esphome/components/cover/__init__.py | 33 +++++--- esphome/components/number/__init__.py | 18 +++- esphome/components/sensor/__init__.py | 20 +++-- esphome/const.py | 87 +++++++++++--------- script/sync-device_class.py | 67 +++++++++++++++ 7 files changed, 169 insertions(+), 62 deletions(-) create mode 100755 script/sync-device_class.py diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 600c9efe5a..91f63de9e3 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -27,13 +27,13 @@ from esphome.const import ( CONF_TIMING, CONF_TRIGGER_ID, CONF_MQTT_ID, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_GAS, DEVICE_CLASS_HEAT, @@ -62,13 +62,13 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_GAS, DEVICE_CLASS_HEAT, diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index b0611a62e9..7329a7955d 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_ON_PRESS, CONF_TRIGGER_ID, CONF_MQTT_ID, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) @@ -21,6 +22,7 @@ CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ + DEVICE_CLASS_EMPTY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ] diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index d2421f07d9..90e5ee1f03 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -17,6 +17,17 @@ from esphome.const import ( CONF_STOP, CONF_MQTT_ID, CONF_TRIGGER_ID, + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -25,17 +36,17 @@ IS_PLATFORM_COMPONENT = True CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - "", - "awning", - "blind", - "curtain", - "damper", - "door", - "garage", - "gate", - "shade", - "shutter", - "window", + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, ] cover_ns = cg.esphome_ns.namespace("cover") diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 463557e3b3..1d2dac9bde 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -17,19 +17,23 @@ from esphome.const import ( CONF_VALUE, CONF_OPERATION, CONF_CYCLE, - DEVICE_CLASS_DISTANCE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATA_RATE, + DEVICE_CLASS_DATA_SIZE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_IRRADIANCE, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, @@ -46,6 +50,7 @@ from esphome.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, @@ -53,8 +58,8 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, DEVICE_CLASS_WATER, - DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WEIGHT, + DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -63,10 +68,13 @@ CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATA_RATE, + DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, @@ -74,6 +82,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_IRRADIANCE, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, @@ -83,13 +92,14 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, - DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 7842cef4de..54fdfcffdc 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -29,21 +29,25 @@ from esphome.const import ( CONF_WINDOW_SIZE, CONF_MQTT_ID, CONF_FORCE_UPDATE, - DEVICE_CLASS_DISTANCE, - DEVICE_CLASS_DURATION, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATA_RATE, + DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DATE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_IRRADIANCE, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, @@ -60,6 +64,7 @@ from esphome.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, @@ -68,8 +73,8 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, DEVICE_CLASS_WATER, - DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WEIGHT, + DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass @@ -78,21 +83,25 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - DEVICE_CLASS_EMPTY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATA_RATE, + DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DATE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_IRRADIANCE, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MONETARY, DEVICE_CLASS_NITROGEN_DIOXIDE, @@ -109,6 +118,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SOUND_PRESSURE, DEVICE_CLASS_SPEED, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, diff --git a/esphome/const.py b/esphome/const.py index e5d0b0bb10..bd593bbc80 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -916,80 +916,87 @@ UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" UNIT_WATT_HOURS = "Wh" -# device classes of binary_sensor component -DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" -DEVICE_CLASS_COLD = "cold" -DEVICE_CLASS_CONNECTIVITY = "connectivity" -DEVICE_CLASS_DOOR = "door" -DEVICE_CLASS_GARAGE_DOOR = "garage_door" -DEVICE_CLASS_HEAT = "heat" -DEVICE_CLASS_LIGHT = "light" -DEVICE_CLASS_LOCK = "lock" -DEVICE_CLASS_MOTION = "motion" -DEVICE_CLASS_MOVING = "moving" -DEVICE_CLASS_OCCUPANCY = "occupancy" -DEVICE_CLASS_OPENING = "opening" -DEVICE_CLASS_PLUG = "plug" -DEVICE_CLASS_PRESENCE = "presence" -DEVICE_CLASS_PROBLEM = "problem" -DEVICE_CLASS_RUNNING = "running" -DEVICE_CLASS_SAFETY = "safety" -DEVICE_CLASS_SMOKE = "smoke" -DEVICE_CLASS_SOUND = "sound" -DEVICE_CLASS_TAMPER = "tamper" -DEVICE_CLASS_VIBRATION = "vibration" -DEVICE_CLASS_WINDOW = "window" -# device classes of both binary_sensor and sensor component -DEVICE_CLASS_EMPTY = "" -DEVICE_CLASS_BATTERY = "battery" -DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" -DEVICE_CLASS_GAS = "gas" -DEVICE_CLASS_MOISTURE = "moisture" -DEVICE_CLASS_POWER = "power" -# device classes of sensor component +# device classes DEVICE_CLASS_APPARENT_POWER = "apparent_power" DEVICE_CLASS_AQI = "aqi" +DEVICE_CLASS_ATMOSPHERIC_PRESSURE = "atmospheric_pressure" +DEVICE_CLASS_AWNING = "awning" +DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_BATTERY_CHARGING = "battery_charging" +DEVICE_CLASS_BLIND = "blind" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" +DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" +DEVICE_CLASS_COLD = "cold" +DEVICE_CLASS_CONNECTIVITY = "connectivity" DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_CURTAIN = "curtain" +DEVICE_CLASS_DAMPER = "damper" +DEVICE_CLASS_DATA_RATE = "data_rate" +DEVICE_CLASS_DATA_SIZE = "data_size" DEVICE_CLASS_DATE = "date" DEVICE_CLASS_DISTANCE = "distance" +DEVICE_CLASS_DOOR = "door" DEVICE_CLASS_DURATION = "duration" +DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_FREQUENCY = "frequency" +DEVICE_CLASS_GARAGE = "garage" +DEVICE_CLASS_GARAGE_DOOR = "garage_door" +DEVICE_CLASS_GAS = "gas" +DEVICE_CLASS_GATE = "gate" +DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" +DEVICE_CLASS_IRRADIANCE = "irradiance" +DEVICE_CLASS_LIGHT = "light" +DEVICE_CLASS_LOCK = "lock" +DEVICE_CLASS_MOISTURE = "moisture" DEVICE_CLASS_MONETARY = "monetary" +DEVICE_CLASS_MOTION = "motion" +DEVICE_CLASS_MOVING = "moving" DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide" DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OCCUPANCY = "occupancy" +DEVICE_CLASS_OPENING = "opening" +DEVICE_CLASS_OUTLET = "outlet" DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PLUG = "plug" DEVICE_CLASS_PM1 = "pm1" DEVICE_CLASS_PM10 = "pm10" DEVICE_CLASS_PM25 = "pm25" +DEVICE_CLASS_POWER = "power" DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRECIPITATION = "precipitation" DEVICE_CLASS_PRECIPITATION_INTENSITY = "precipitation_intensity" +DEVICE_CLASS_PRESENCE = "presence" DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_PROBLEM = "problem" DEVICE_CLASS_REACTIVE_POWER = "reactive_power" +DEVICE_CLASS_RESTART = "restart" +DEVICE_CLASS_RUNNING = "running" +DEVICE_CLASS_SAFETY = "safety" +DEVICE_CLASS_SHADE = "shade" +DEVICE_CLASS_SHUTTER = "shutter" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_SMOKE = "smoke" +DEVICE_CLASS_SOUND = "sound" +DEVICE_CLASS_SOUND_PRESSURE = "sound_pressure" DEVICE_CLASS_SPEED = "speed" DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" +DEVICE_CLASS_SWITCH = "switch" +DEVICE_CLASS_TAMPER = "tamper" DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" +DEVICE_CLASS_UPDATE = "update" +DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLTAGE = "voltage" DEVICE_CLASS_VOLUME = "volume" DEVICE_CLASS_WATER = "water" -DEVICE_CLASS_WIND_SPEED = "wind_speed" DEVICE_CLASS_WEIGHT = "weight" -# device classes of both binary_sensor and button component -DEVICE_CLASS_UPDATE = "update" -# device classes of button component -DEVICE_CLASS_RESTART = "restart" -# device classes of switch component -DEVICE_CLASS_OUTLET = "outlet" -DEVICE_CLASS_SWITCH = "switch" - +DEVICE_CLASS_WINDOW = "window" +DEVICE_CLASS_WIND_SPEED = "wind_speed" # state classes STATE_CLASS_NONE = "" diff --git a/script/sync-device_class.py b/script/sync-device_class.py new file mode 100755 index 0000000000..882655561b --- /dev/null +++ b/script/sync-device_class.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +import re + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.number import NumberDeviceClass +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.switch import SwitchDeviceClass + +BLOCKLIST = ( + # requires special support on HA side + "enum", +) + +DOMAINS = { + "binary_sensor": BinarySensorDeviceClass, + "button": ButtonDeviceClass, + "cover": CoverDeviceClass, + "number": NumberDeviceClass, + "sensor": SensorDeviceClass, + "switch": SwitchDeviceClass, +} + + +def sub(path, pattern, repl): + with open(path, "r") as handle: + content = handle.read() + content = re.sub(pattern, repl, content, flags=re.MULTILINE, count=1) + with open(path, "w") as handle: + handle.write(content) + + +def main(): + classes = {"EMPTY": ""} + allowed = {} + + for domain, enum in DOMAINS.items(): + available = { + cls.value.upper(): cls.value for cls in enum if cls.value not in BLOCKLIST + } + + classes.update(available) + allowed[domain] = list(available.keys()) + ["EMPTY"] + + # replace constant defines in const.py + out = "" + for cls in sorted(classes): + out += f'DEVICE_CLASS_{cls.upper()} = "{classes[cls]}"\n' + sub("esphome/const.py", '(DEVICE_CLASS_\w+ = "\w*"\r?\n)+', out) + + for domain in sorted(allowed): + # replace imports + out = "" + for item in sorted(allowed[domain]): + out += f" DEVICE_CLASS_{item.upper()},\n" + + sub( + f"esphome/components/{domain}/__init__.py", + "( DEVICE_CLASS_\w+,\r?\n)+", + out, + ) + + +if __name__ == "__main__": + main() From 37d55b55fcc6f7f0428361ed0516218ec617a510 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 17 Feb 2023 19:06:18 +1300 Subject: [PATCH 029/115] Fix adoption of variants and pico-w (#4455) --- .../components/dashboard_import/__init__.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index e1bd6a7f08..6b6750cbf4 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -83,11 +83,30 @@ def import_config( raise FileExistsError if project_name == "esphome.web": + if "esp32c3" in import_url: + board = "esp32-c3-devkitm-1" + platform = "ESP32" + elif "esp32s2" in import_url: + board = "esp32-s2-saola-1" + platform = "ESP32" + elif "esp32s3" in import_url: + board = "esp32-s3-devkitc-1" + platform = "ESP32" + elif "esp32" in import_url: + board = "esp32dev" + platform = "ESP32" + elif "esp8266" in import_url: + board = "esp01_1m" + platform = "ESP8266" + elif "pico-w" in import_url: + board = "pico-w" + platform = "RP2040" + kwargs = { "name": name, "friendly_name": friendly_name, - "platform": "ESP32" if "esp32" in import_url else "ESP8266", - "board": "esp32dev" if "esp32" in import_url else "esp01_1m", + "platform": platform, + "board": board, "ssid": "!secret wifi_ssid", "psk": "!secret wifi_password", } From c03b1fae68731719c7deb515ef6cd5c2c7290212 Mon Sep 17 00:00:00 2001 From: jmichiel <194387+jmichiel@users.noreply.github.com> Date: Sun, 19 Feb 2023 14:33:52 +0100 Subject: [PATCH 030/115] fix preset discovery config (#4451) Co-authored-by: Michiel, Jeroen --- esphome/components/mqtt/mqtt_climate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 8dd03dd5c8..1947e02b9e 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -72,7 +72,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // preset_mode_state_topic root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); // presets - JsonArray presets = root.createNestedArray("presets"); + JsonArray presets = root.createNestedArray("preset_modes"); if (traits.supports_preset(CLIMATE_PRESET_HOME)) presets.add("home"); if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { From 66eecd36758f5d36e802edde6101473fd73a9207 Mon Sep 17 00:00:00 2001 From: Adam Jacques Date: Sun, 19 Feb 2023 05:38:27 -0800 Subject: [PATCH 031/115] NeoPixel - Add support for ESP32-S3 (#4435) --- esphome/components/neopixelbus/_methods.py | 3 +++ esphome/components/neopixelbus/light.py | 3 ++- script/test | 1 + tests/README.md | 1 + tests/test8.yaml | 27 ++++++++++++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/test8.yaml diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py index 98a2d152e1..a290257d6b 100644 --- a/esphome/components/neopixelbus/_methods.py +++ b/esphome/components/neopixelbus/_methods.py @@ -15,6 +15,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3, + VARIANT_ESP32S3, ) from esphome.core import CORE from .const import ( @@ -58,6 +59,7 @@ SPI_SPEEDS = [40e6, 20e6, 10e6, 5e6, 2e6, 1e6, 500e3] def _esp32_rmt_default_channel(): return { VARIANT_ESP32S2: 1, + VARIANT_ESP32S3: 1, VARIANT_ESP32C3: 1, }.get(get_esp32_variant(), 6) @@ -70,6 +72,7 @@ def _validate_esp32_rmt_channel(value): variant_channels = { VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7, CHANNEL_DYNAMIC], VARIANT_ESP32S2: [0, 1, 2, 3, CHANNEL_DYNAMIC], + VARIANT_ESP32S3: [0, 1, 2, 3, CHANNEL_DYNAMIC], VARIANT_ESP32C3: [0, 1, CHANNEL_DYNAMIC], } variant = get_esp32_variant() diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 722e6f5b06..072a565eda 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -220,4 +220,5 @@ async def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library("makuna/NeoPixelBus", "2.6.9") + # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions + cg.add_library("makuna/NeoPixelBus", "2.7.3") diff --git a/script/test b/script/test index 9f5dca65fa..36be9118ed 100755 --- a/script/test +++ b/script/test @@ -11,3 +11,4 @@ esphome compile tests/test2.yaml esphome compile tests/test3.yaml esphome compile tests/test4.yaml esphome compile tests/test5.yaml +esphome compile tests/test8.yaml diff --git a/tests/README.md b/tests/README.md index 3238acaa79..6d83fc6886 100644 --- a/tests/README.md +++ b/tests/README.md @@ -26,3 +26,4 @@ Current test_.yaml file contents. | test5.yaml | ESP32 | wifi | ble_server | test6.yaml | RP2040 | wifi | N/A | test7.yaml | ESP32-C3 | wifi | N/A +| test8.yaml | ESP32-S3 | wifi | None diff --git a/tests/test8.yaml b/tests/test8.yaml new file mode 100644 index 0000000000..c423ecbce6 --- /dev/null +++ b/tests/test8.yaml @@ -0,0 +1,27 @@ +# Tests for ESP32-S3 boards +--- +wifi: + ssid: "ssid" + +esp32: + board: esp32-c3-devkitm-1 + variant: ESP32S3 + framework: + type: arduino + +esphome: + name: "esp32-s3-test" + +logger: + +light: + - platform: neopixelbus + type: GRB + variant: WS2812 + pin: 33 + num_leds: 1 + id: neopixel + method: esp32_rmt + name: "neopixel-enable" + internal: false + restore_mode: ALWAYS_OFF From 77db8c84015350ce46191cf8ca58e2d00e950600 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sun, 19 Feb 2023 21:11:21 +0200 Subject: [PATCH 032/115] add SUB_BINARY_SENSOR macro (#4447) --- esphome/components/binary_sensor/binary_sensor.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 730ded3f94..fbfff63a38 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -19,6 +19,15 @@ namespace binary_sensor { } \ } +#define SUB_BINARY_SENSOR(name) \ + protected: \ + binary_sensor::BinarySensor *name##_binary_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { \ + this->name##_binary_sensor_ = binary_sensor; \ + } + /** Base class for all binary_sensor-type classes. * * This class includes a callback that components such as MQTT can subscribe to for state changes. From 12bef16d54686cb6a52a579c3fb1b6fdd11a2c45 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sun, 19 Feb 2023 21:11:24 +0200 Subject: [PATCH 033/115] add SUB_TEXT_SENSOR macro (#4448) --- esphome/components/text_sensor/text_sensor.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index a7673ed9ff..60e9e30b6e 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -21,6 +21,13 @@ namespace text_sensor { } \ } +#define SUB_TEXT_SENSOR(name) \ + protected: \ + text_sensor::TextSensor *name##_text_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } + class TextSensor : public EntityBase { public: explicit TextSensor(); From 36a1f6cfb11b05de2c2cc199832f053ee50e01cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 20 Feb 2023 08:12:29 +1300 Subject: [PATCH 034/115] Update Manifest to rmeove unused dashboard files and include .c ethernet drivers (#4459) --- MANIFEST.in | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index a3126404f2..45d5e86672 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,6 @@ include LICENSE include README.md include requirements.txt -include esphome/dashboard/templates/*.html -recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE -recursive-include esphome *.cpp *.h *.tcc +recursive-include esphome *.cpp *.h *.tcc *.c recursive-include esphome *.py.script recursive-include esphome LICENSE.txt From 40e2832e67fe24c0d581f387cfacf1a5fd781b01 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Feb 2023 14:20:13 -0500 Subject: [PATCH 035/115] Simplify sensor schema generation (#4462) * Simplify sensor schema generation * Mark class not optional * Fix assignment --- esphome/components/sensor/__init__.py | 58 ++++++++------------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 54fdfcffdc..d3cb39c2f6 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -272,48 +272,24 @@ def sensor_schema( state_class: str = _UNDEF, entity_category: str = _UNDEF, ) -> cv.Schema: - schema = SENSOR_SCHEMA + schema = {} + if class_ is not _UNDEF: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if unit_of_measurement is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement - ): validate_unit_of_measurement - } - ) - if icon is not _UNDEF: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): validate_icon}) - if accuracy_decimals is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ACCURACY_DECIMALS, default=accuracy_decimals - ): validate_accuracy_decimals, - } - ) - if device_class is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if state_class is not _UNDEF: - schema = schema.extend( - {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} - ) - if entity_category is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - return schema + # Not optional. + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_UNIT_OF_MEASUREMENT, unit_of_measurement, validate_unit_of_measurement), + (CONF_ICON, icon, validate_icon), + (CONF_ACCURACY_DECIMALS, accuracy_decimals, validate_accuracy_decimals), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_STATE_CLASS, state_class, validate_state_class), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return SENSOR_SCHEMA.extend(schema) @FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_) From e68beb8a439e1e94278a412fb8853e8034178293 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sun, 19 Feb 2023 21:54:00 +0200 Subject: [PATCH 036/115] add SUB_NUMBER macro and schema to number (#4449) * add SUB_NUMBER macro and schema * add SUB_NUMBER macro and schema * add SUB_NUMBER macro and schema --- esphome/components/copy/number/__init__.py | 15 +++--- esphome/components/demo/__init__.py | 5 +- .../modbus_controller/number/__init__.py | 5 +- esphome/components/number/__init__.py | 49 ++++++++++++++++++- esphome/components/number/number.h | 7 +++ .../components/template/number/__init__.py | 7 +-- esphome/components/tuya/number/__init__.py | 7 +-- 7 files changed, 77 insertions(+), 18 deletions(-) diff --git a/esphome/components/copy/number/__init__.py b/esphome/components/copy/number/__init__.py index 4e78627a1f..204518da39 100644 --- a/esphome/components/copy/number/__init__.py +++ b/esphome/components/copy/number/__init__.py @@ -15,12 +15,15 @@ from .. import copy_ns CopyNumber = copy_ns.class_("CopyNumber", number.Number, cg.Component) -CONFIG_SCHEMA = number.NUMBER_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyNumber), - cv.Required(CONF_SOURCE_ID): cv.use_id(number.Number), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + number.number_schema(CopyNumber) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(number.Number), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 19b35828a5..05160bf8cb 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -284,9 +284,10 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - number.NUMBER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + number.number_schema(DemoNumber) + .extend(cv.COMPONENT_SCHEMA) + .extend( { - cv.GenerateID(): cv.declare_id(DemoNumber), cv.Required(CONF_TYPE): cv.enum(NUMBER_TYPES, int=True), cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_MAX_VALUE): cv.float_, diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 52f63e791b..fe99b28a00 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -60,9 +60,10 @@ def validate_modbus_number(config): CONFIG_SCHEMA = cv.All( - number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema).extend( + number.number_schema(ModbusNumber) + .extend(ModbusItemBaseSchema) + .extend( { - cv.GenerateID(): cv.declare_id(ModbusNumber), cv.Optional(CONF_REGISTER_TYPE, default="holding"): cv.enum( MODBUS_WRITE_REGISTER_TYPE ), diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 1d2dac9bde..a51cf13124 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -7,7 +7,9 @@ from esphome.const import ( CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, CONF_ID, + CONF_ICON, CONF_MODE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -63,6 +65,7 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity +from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ @@ -150,8 +153,8 @@ NUMBER_OPERATION_OPTIONS = { "TO_MAX": NumberOperation.NUMBER_OP_TO_MAX, } -icon = cv.icon validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +validate_unit_of_measurement = cv.string_strict NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -170,12 +173,54 @@ NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string_strict, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, } ) +_UNDEF = object() + + +def number_schema( + class_: MockObjClass = _UNDEF, + *, + icon: str = _UNDEF, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, + unit_of_measurement: str = _UNDEF, +) -> cv.Schema: + schema = NUMBER_SCHEMA + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) + if unit_of_measurement is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement + ): validate_unit_of_measurement + } + ) + return schema + async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: Optional[float] diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 4f63e0480c..d839d12ad1 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -23,6 +23,13 @@ namespace number { } \ } +#define SUB_NUMBER(name) \ + protected: \ + number::Number *name##_number_{nullptr}; \ +\ + public: \ + void set_##name##_number(number::Number *number) { this->name##_number_ = number; } + class Number; /** Base-class for all numbers. diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 3dec7066d3..b9a507c7e9 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -46,9 +46,9 @@ def validate(config): CONFIG_SCHEMA = cv.All( - number.NUMBER_SCHEMA.extend( + number.number_schema(TemplateNumber) + .extend( { - cv.GenerateID(): cv.declare_id(TemplateNumber), cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, @@ -58,7 +58,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INITIAL_VALUE): cv.float_, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate_min_max, validate, ) diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index 12c0c0f6e5..42ac9fcfbe 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -23,16 +23,17 @@ def validate_min_max(config): CONFIG_SCHEMA = cv.All( - number.NUMBER_SCHEMA.extend( + number.number_schema(TuyaNumber) + .extend( { - cv.GenerateID(): cv.declare_id(TuyaNumber), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Required(CONF_NUMBER_DATAPOINT): cv.uint8_t, cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_min_max, ) From 72391389a31c6000a02af63a80fb3806b8a2910c Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sun, 19 Feb 2023 21:54:03 +0200 Subject: [PATCH 037/115] add SUB_BUTTON macro and ability to button schema to define the class (#4450) * add ability to button schema to define the class * add SUB_BUTTON macro --- esphome/components/button/__init__.py | 5 +++++ esphome/components/button/button.h | 7 +++++++ esphome/components/copy/button/__init__.py | 3 +-- .../components/factory_reset/button/__init__.py | 15 ++++++--------- esphome/components/restart/button/__init__.py | 12 +++++------- esphome/components/safe_mode/button/__init__.py | 2 +- esphome/components/shutdown/button/__init__.py | 8 +++----- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 7329a7955d..9d250bbc8b 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity +from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -56,11 +57,15 @@ _UNDEF = object() def button_schema( + class_: MockObjClass = _UNDEF, + *, icon: str = _UNDEF, entity_category: str = _UNDEF, device_class: str = _UNDEF, ) -> cv.Schema: schema = BUTTON_SCHEMA + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if icon is not _UNDEF: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) if entity_category is not _UNDEF: diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 398d398cd9..3b5f338887 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -15,6 +15,13 @@ namespace button { } \ } +#define SUB_BUTTON(name) \ + protected: \ + button::Button *name##_button_{nullptr}; \ +\ + public: \ + void set_##name##_button(button::Button *button) { this->name##_button_ = button; } + /** Base class for all buttons. * * A button is just a momentary switch that does not have a state, only a trigger. diff --git a/esphome/components/copy/button/__init__.py b/esphome/components/copy/button/__init__.py index 65d956601a..626a5a8db1 100644 --- a/esphome/components/copy/button/__init__.py +++ b/esphome/components/copy/button/__init__.py @@ -16,10 +16,9 @@ CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component) CONFIG_SCHEMA = ( - button.button_schema() + button.button_schema(CopyButton) .extend( { - cv.GenerateID(): cv.declare_id(CopyButton), cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button), } ) diff --git a/esphome/components/factory_reset/button/__init__.py b/esphome/components/factory_reset/button/__init__.py index d5beac34b5..010691ac7f 100644 --- a/esphome/components/factory_reset/button/__init__.py +++ b/esphome/components/factory_reset/button/__init__.py @@ -13,15 +13,12 @@ FactoryResetButton = factory_reset_ns.class_( "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/restart/button/__init__.py b/esphome/components/restart/button/__init__.py index 1a0e9cdc3d..1b2c991261 100644 --- a/esphome/components/restart/button/__init__.py +++ b/esphome/components/restart/button/__init__.py @@ -10,13 +10,11 @@ from esphome.const import ( restart_ns = cg.esphome_ns.namespace("restart") RestartButton = restart_ns.class_("RestartButton", button.Button, cg.Component) -CONFIG_SCHEMA = ( - button.button_schema( - device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG - ) - .extend({cv.GenerateID(): cv.declare_id(RestartButton)}) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py index 2cd8892afb..307e4e372e 100644 --- a/esphome/components/safe_mode/button/__init__.py +++ b/esphome/components/safe_mode/button/__init__.py @@ -17,11 +17,11 @@ SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Compone CONFIG_SCHEMA = ( button.button_schema( + SafeModeButton, device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, ) - .extend({cv.GenerateID(): cv.declare_id(SafeModeButton)}) .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)}) .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/shutdown/button/__init__.py b/esphome/components/shutdown/button/__init__.py index 51cd6d6da2..79d0b23935 100644 --- a/esphome/components/shutdown/button/__init__.py +++ b/esphome/components/shutdown/button/__init__.py @@ -10,11 +10,9 @@ from esphome.const import ( shutdown_ns = cg.esphome_ns.namespace("shutdown") ShutdownButton = shutdown_ns.class_("ShutdownButton", button.Button, cg.Component) -CONFIG_SCHEMA = ( - button.button_schema(entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER) - .extend({cv.GenerateID(): cv.declare_id(ShutdownButton)}) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = button.button_schema( + ShutdownButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): From add40c765299a959db67a77047df3022e8aeb48d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 20 Feb 2023 10:13:35 +1300 Subject: [PATCH 038/115] Simplify number_schema function (#4467) --- esphome/components/number/__init__.py | 45 ++++++++------------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index a51cf13124..70c53cace3 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -159,7 +159,6 @@ validate_unit_of_measurement = cv.string_strict NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), - cv.GenerateID(): cv.declare_id(Number), cv.Optional(CONF_ON_VALUE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), @@ -183,43 +182,25 @@ _UNDEF = object() def number_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass, *, icon: str = _UNDEF, entity_category: str = _UNDEF, device_class: str = _UNDEF, unit_of_measurement: str = _UNDEF, ) -> cv.Schema: - schema = NUMBER_SCHEMA - if class_ is not _UNDEF: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if icon is not _UNDEF: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - if entity_category is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - if device_class is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if unit_of_measurement is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement - ): validate_unit_of_measurement - } - ) - return schema + schema = {cv.GenerateID(): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_UNIT_OF_MEASUREMENT, unit_of_measurement, validate_unit_of_measurement), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return NUMBER_SCHEMA.extend(schema) async def setup_number_core_( From 04c12823b5187f40ed99812f715a2e853b1a54ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 20 Feb 2023 10:13:37 +1300 Subject: [PATCH 039/115] Simplify button_schema function (#4468) --- esphome/components/button/__init__.py | 35 +++++++------------ esphome/components/output/button/__init__.py | 17 +++++---- .../components/template/button/__init__.py | 7 +--- esphome/components/wake_on_lan/button.py | 5 +-- 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 9d250bbc8b..55f2fe794a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -57,34 +57,23 @@ _UNDEF = object() def button_schema( - class_: MockObjClass = _UNDEF, + class_: MockObjClass, *, icon: str = _UNDEF, entity_category: str = _UNDEF, device_class: str = _UNDEF, ) -> cv.Schema: - schema = BUTTON_SCHEMA - if class_ is not _UNDEF: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if icon is not _UNDEF: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - if entity_category is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - if device_class is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - return schema + schema = {cv.GenerateID(): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return BUTTON_SCHEMA.extend(schema) async def setup_button_core_(var, config): diff --git a/esphome/components/output/button/__init__.py b/esphome/components/output/button/__init__.py index 4b81cedc0c..c31865ccfb 100644 --- a/esphome/components/output/button/__init__.py +++ b/esphome/components/output/button/__init__.py @@ -6,13 +6,16 @@ from .. import output_ns OutputButton = output_ns.class_("OutputButton", button.Button, cg.Component) -CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(OutputButton), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Required(CONF_DURATION): cv.positive_time_period_milliseconds, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + button.button_schema(OutputButton) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Required(CONF_DURATION): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/template/button/__init__.py b/esphome/components/template/button/__init__.py index 2ad5e54c80..e0101dfc8f 100644 --- a/esphome/components/template/button/__init__.py +++ b/esphome/components/template/button/__init__.py @@ -1,15 +1,10 @@ -import esphome.config_validation as cv from esphome.components import button from .. import template_ns TemplateButton = template_ns.class_("TemplateButton", button.Button) -CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateButton), - } -) +CONFIG_SCHEMA = button.button_schema(TemplateButton) async def to_code(config): diff --git a/esphome/components/wake_on_lan/button.py b/esphome/components/wake_on_lan/button.py index 2710eb3df9..778ea60cfa 100644 --- a/esphome/components/wake_on_lan/button.py +++ b/esphome/components/wake_on_lan/button.py @@ -12,11 +12,12 @@ WakeOnLanButton = wake_on_lan_ns.class_("WakeOnLanButton", button.Button, cg.Com DEPENDENCIES = ["network"] CONFIG_SCHEMA = cv.All( - button.BUTTON_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + button.button_schema(WakeOnLanButton) + .extend(cv.COMPONENT_SCHEMA) + .extend( cv.Schema( { cv.Required(CONF_TARGET_MAC_ADDRESS): cv.mac_address, - cv.GenerateID(): cv.declare_id(WakeOnLanButton), } ), ), From 5c49730cb9c7132e58a7991c6f9f9d6a840a12c7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 20 Feb 2023 10:13:40 +1300 Subject: [PATCH 040/115] Simplify binary_sensor_schema function (#4469) --- .../analog_threshold/binary_sensor.py | 28 +++++++++------ esphome/components/binary_sensor/__init__.py | 35 ++++++++----------- .../matrix_keypad/binary_sensor/__init__.py | 3 +- esphome/components/tm1637/binary_sensor.py | 3 +- .../tm1638/binary_sensor/__init__.py | 3 +- 5 files changed, 34 insertions(+), 38 deletions(-) diff --git a/esphome/components/analog_threshold/binary_sensor.py b/esphome/components/analog_threshold/binary_sensor.py index ef4a6044bf..7b964dfae6 100644 --- a/esphome/components/analog_threshold/binary_sensor.py +++ b/esphome/components/analog_threshold/binary_sensor.py @@ -15,18 +15,24 @@ AnalogThresholdBinarySensor = analog_threshold_ns.class_( CONF_UPPER = "upper" CONF_LOWER = "lower" -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(AnalogThresholdBinarySensor), - cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), - cv.Required(CONF_THRESHOLD): cv.Any( - cv.float_, - cv.Schema( - {cv.Required(CONF_UPPER): cv.float_, cv.Required(CONF_LOWER): cv.float_} +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(AnalogThresholdBinarySensor) + .extend( + { + cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_THRESHOLD): cv.Any( + cv.float_, + cv.Schema( + { + cv.Required(CONF_UPPER): cv.float_, + cv.Required(CONF_LOWER): cv.float_, + } + ), ), - ), - } -).extend(cv.COMPONENT_SCHEMA) + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 91f63de9e3..f4a5c95b12 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -393,28 +393,21 @@ def binary_sensor_schema( entity_category: str = _UNDEF, device_class: str = _UNDEF, ) -> cv.Schema: - schema = BINARY_SENSOR_SCHEMA + schema = {} + if class_ is not _UNDEF: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if icon is not _UNDEF: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - if entity_category is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - if device_class is not _UNDEF: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - return schema + # Not cv.optional + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + ]: + if default is not _UNDEF: + schema[cv.Optional(key, default=default)] = validator + + return BINARY_SENSOR_SCHEMA.extend(schema) async def setup_binary_sensor_core_(var, config): diff --git a/esphome/components/matrix_keypad/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py index 204db98650..9ad909f60a 100644 --- a/esphome/components/matrix_keypad/binary_sensor/__init__.py +++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py @@ -30,9 +30,8 @@ def check_button(obj): CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(MatrixKeypadBinarySensor).extend( { - cv.GenerateID(): cv.declare_id(MatrixKeypadBinarySensor), cv.GenerateID(CONF_KEYPAD_ID): cv.use_id(MatrixKeypad), cv.Optional(CONF_ROW): cv.int_, cv.Optional(CONF_COL): cv.int_, diff --git a/esphome/components/tm1637/binary_sensor.py b/esphome/components/tm1637/binary_sensor.py index 66b5172358..f14b9bd018 100644 --- a/esphome/components/tm1637/binary_sensor.py +++ b/esphome/components/tm1637/binary_sensor.py @@ -9,9 +9,8 @@ tm1637_ns = cg.esphome_ns.namespace("tm1637") TM1637Display = tm1637_ns.class_("TM1637Display", cg.PollingComponent) TM1637Key = tm1637_ns.class_("TM1637Key", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TM1637Key).extend( { - cv.GenerateID(): cv.declare_id(TM1637Key), cv.GenerateID(CONF_TM1637_ID): cv.use_id(TM1637Display), cv.Required(CONF_KEY): cv.int_range(min=0, max=15), } diff --git a/esphome/components/tm1638/binary_sensor/__init__.py b/esphome/components/tm1638/binary_sensor/__init__.py index 7262d9e9e1..6623228555 100644 --- a/esphome/components/tm1638/binary_sensor/__init__.py +++ b/esphome/components/tm1638/binary_sensor/__init__.py @@ -6,9 +6,8 @@ from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID TM1638Key = tm1638_ns.class_("TM1638Key", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TM1638Key).extend( { - cv.GenerateID(): cv.declare_id(TM1638Key), cv.GenerateID(CONF_TM1638_ID): cv.use_id(TM1638Component), cv.Required(CONF_KEY): cv.int_range(min=0, max=15), } From f68d5779868b595e812d817c124feb56e09948a0 Mon Sep 17 00:00:00 2001 From: irtimaled Date: Sun, 19 Feb 2023 13:50:46 -0800 Subject: [PATCH 041/115] Add configurable color datapoint (#4383) * Add configurable color datapoint * Lint fixes * Review comments * Linting --- esphome/components/tuya/light/__init__.py | 30 +++-- esphome/components/tuya/light/tuya_light.cpp | 116 +++++++++++-------- esphome/components/tuya/light/tuya_light.h | 14 ++- 3 files changed, 97 insertions(+), 63 deletions(-) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index b983e3f84e..d806060018 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -23,9 +23,23 @@ CONF_COLOR_TEMPERATURE_INVERT = "color_temperature_invert" CONF_COLOR_TEMPERATURE_MAX_VALUE = "color_temperature_max_value" CONF_RGB_DATAPOINT = "rgb_datapoint" CONF_HSV_DATAPOINT = "hsv_datapoint" +CONF_COLOR_DATAPOINT = "color_datapoint" +CONF_COLOR_TYPE = "color_type" + +TuyaColorType = tuya_ns.enum("TuyaColorType") + +COLOR_TYPES = { + "RGB": TuyaColorType.RGB, + "HSV": TuyaColorType.HSV, + "RGBHSV": TuyaColorType.RGBHSV, +} TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) +COLOR_CONFIG_ERROR = ( + "This option has been removed, use color_datapoint and color_type instead." +) + CONFIG_SCHEMA = cv.All( light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( { @@ -34,8 +48,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, - cv.Exclusive(CONF_RGB_DATAPOINT, "color"): cv.uint8_t, - cv.Exclusive(CONF_HSV_DATAPOINT, "color"): cv.uint8_t, + cv.Optional(CONF_RGB_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), + cv.Optional(CONF_HSV_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), + cv.Inclusive(CONF_COLOR_DATAPOINT, "color"): cv.uint8_t, + cv.Inclusive(CONF_COLOR_TYPE, "color"): cv.enum(COLOR_TYPES, upper=True), cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, cv.Inclusive( CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature" @@ -61,8 +77,7 @@ CONFIG_SCHEMA = cv.All( cv.has_at_least_one_key( CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT, - CONF_RGB_DATAPOINT, - CONF_HSV_DATAPOINT, + CONF_COLOR_DATAPOINT, ), ) @@ -78,10 +93,9 @@ async def to_code(config): cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT])) if CONF_SWITCH_DATAPOINT in config: cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) - if CONF_RGB_DATAPOINT in config: - cg.add(var.set_rgb_id(config[CONF_RGB_DATAPOINT])) - elif CONF_HSV_DATAPOINT in config: - cg.add(var.set_hsv_id(config[CONF_HSV_DATAPOINT])) + if CONF_COLOR_DATAPOINT in config: + cg.add(var.set_color_id(config[CONF_COLOR_DATAPOINT])) + cg.add(var.set_color_type(config[CONF_COLOR_TYPE])) if CONF_COLOR_TEMPERATURE_DATAPOINT in config: cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT])) cg.add(var.set_color_temperature_invert(config[CONF_COLOR_TEMPERATURE_INVERT])) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index b75e85bc14..869e20871d 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -50,38 +50,39 @@ void TuyaLight::setup() { call.perform(); }); } - if (rgb_id_.has_value()) { - this->parent_->register_listener(*this->rgb_id_, [this](const TuyaDatapoint &datapoint) { - auto red = parse_hex(datapoint.value_string.substr(0, 2)); - auto green = parse_hex(datapoint.value_string.substr(2, 2)); - auto blue = parse_hex(datapoint.value_string.substr(4, 2)); - if (red.has_value() && green.has_value() && blue.has_value()) { - if (this->state_->current_values != this->state_->remote_values) { - ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored"); - return; - } - - auto call = this->state_->make_call(); - call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); - call.perform(); + if (color_id_.has_value()) { + this->parent_->register_listener(*this->color_id_, [this](const TuyaDatapoint &datapoint) { + if (this->state_->current_values != this->state_->remote_values) { + ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored"); + return; } - }); - } else if (hsv_id_.has_value()) { - this->parent_->register_listener(*this->hsv_id_, [this](const TuyaDatapoint &datapoint) { - auto hue = parse_hex(datapoint.value_string.substr(0, 4)); - auto saturation = parse_hex(datapoint.value_string.substr(4, 4)); - auto value = parse_hex(datapoint.value_string.substr(8, 4)); - if (hue.has_value() && saturation.has_value() && value.has_value()) { - if (this->state_->current_values != this->state_->remote_values) { - ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored"); - return; - } - float red, green, blue; - hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); - auto call = this->state_->make_call(); - call.set_rgb(red, green, blue); - call.perform(); + switch (*this->color_type_) { + case TuyaColorType::RGBHSV: + case TuyaColorType::RGB: { + auto red = parse_hex(datapoint.value_string.substr(0, 2)); + auto green = parse_hex(datapoint.value_string.substr(2, 2)); + auto blue = parse_hex(datapoint.value_string.substr(4, 2)); + if (red.has_value() && green.has_value() && blue.has_value()) { + auto rgb_call = this->state_->make_call(); + rgb_call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255); + rgb_call.perform(); + } + break; + } + case TuyaColorType::HSV: { + auto hue = parse_hex(datapoint.value_string.substr(0, 4)); + auto saturation = parse_hex(datapoint.value_string.substr(4, 4)); + auto value = parse_hex(datapoint.value_string.substr(8, 4)); + if (hue.has_value() && saturation.has_value() && value.has_value()) { + float red, green, blue; + hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue); + auto rgb_call = this->state_->make_call(); + rgb_call.set_rgb(red, green, blue); + rgb_call.perform(); + } + break; + } } }); } @@ -98,17 +99,15 @@ void TuyaLight::dump_config() { if (this->switch_id_.has_value()) { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); } - if (this->rgb_id_.has_value()) { - ESP_LOGCONFIG(TAG, " RGB has datapoint ID %u", *this->rgb_id_); - } else if (this->hsv_id_.has_value()) { - ESP_LOGCONFIG(TAG, " HSV has datapoint ID %u", *this->hsv_id_); + if (this->color_id_.has_value()) { + ESP_LOGCONFIG(TAG, " Color has datapoint ID %u", *this->color_id_); } } light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { - if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->color_id_.has_value()) { if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); } else { @@ -119,7 +118,7 @@ light::LightTraits TuyaLight::get_traits() { traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); - } else if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + } else if (this->color_id_.has_value()) { if (this->dimmer_id_.has_value()) { if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); @@ -142,7 +141,7 @@ void TuyaLight::write_state(light::LightState *state) { float red = 0.0f, green = 0.0f, blue = 0.0f; float color_temperature = 0.0f, brightness = 0.0f; - if (this->rgb_id_.has_value() || this->hsv_id_.has_value()) { + if (this->color_id_.has_value()) { if (this->color_temperature_id_.has_value()) { state->current_values_as_rgbct(&red, &green, &blue, &color_temperature, &brightness); } else if (this->dimmer_id_.has_value()) { @@ -178,21 +177,36 @@ void TuyaLight::write_state(light::LightState *state) { } } - if (brightness == 0.0f || !color_interlock_) { - if (this->rgb_id_.has_value()) { - char buffer[7]; - sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); - std::string rgb_value = buffer; - this->parent_->set_string_datapoint_value(*this->rgb_id_, rgb_value); - } else if (this->hsv_id_.has_value()) { - int hue; - float saturation, value; - rgb_to_hsv(red, green, blue, hue, saturation, value); - char buffer[13]; - sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); - std::string hsv_value = buffer; - this->parent_->set_string_datapoint_value(*this->hsv_id_, hsv_value); + if (this->color_id_.has_value() && (brightness == 0.0f || !color_interlock_)) { + std::string color_value; + switch (*this->color_type_) { + case TuyaColorType::RGB: { + char buffer[7]; + sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); + color_value = buffer; + break; + } + case TuyaColorType::HSV: { + int hue; + float saturation, value; + rgb_to_hsv(red, green, blue, hue, saturation, value); + char buffer[13]; + sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); + color_value = buffer; + break; + } + case TuyaColorType::RGBHSV: { + int hue; + float saturation, value; + rgb_to_hsv(red, green, blue, hue, saturation, value); + char buffer[15]; + sprintf(buffer, "%02X%02X%02X%04X%02X%02X", int(red * 255), int(green * 255), int(blue * 255), hue, + int(saturation * 255), int(value * 255)); + color_value = buffer; + break; + } } + this->parent_->set_string_datapoint_value(*this->color_id_, color_value); } if (this->switch_id_.has_value()) { diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index 3d9f25271c..bd9920f18f 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -7,6 +7,12 @@ namespace esphome { namespace tuya { +enum TuyaColorType { + RGB, + HSV, + RGBHSV, +}; + class TuyaLight : public Component, public light::LightOutput { public: void setup() override; @@ -16,8 +22,8 @@ class TuyaLight : public Component, public light::LightOutput { this->min_value_datapoint_id_ = min_value_datapoint_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } - void set_rgb_id(uint8_t rgb_id) { this->rgb_id_ = rgb_id; } - void set_hsv_id(uint8_t hsv_id) { this->hsv_id_ = hsv_id; } + void set_color_id(uint8_t color_id) { this->color_id_ = color_id; } + void set_color_type(TuyaColorType color_type) { this->color_type_ = color_type; } void set_color_temperature_id(uint8_t color_temperature_id) { this->color_temperature_id_ = color_temperature_id; } void set_color_temperature_invert(bool color_temperature_invert) { this->color_temperature_invert_ = color_temperature_invert; @@ -48,8 +54,8 @@ class TuyaLight : public Component, public light::LightOutput { optional dimmer_id_{}; optional min_value_datapoint_id_{}; optional switch_id_{}; - optional rgb_id_{}; - optional hsv_id_{}; + optional color_id_{}; + optional color_type_{}; optional color_temperature_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; From ed801f7a279f9d4f9a166d3fb5c311de8a5cea21 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Mon, 20 Feb 2023 02:47:37 +0000 Subject: [PATCH 042/115] Add internal_temperature component (#4330) * Add cpu_temperature component * Add tests * Fix formatting * Possible fix for "sensor not shown in HomeAssistant" * Rename component to internal_temperature * Update esphome/components/internal_temperature/internal_temperature.cpp Co-authored-by: Oxan van Leeuwen * Update esphome/components/internal_temperature/internal_temperature.cpp Co-authored-by: Oxan van Leeuwen * Update esphome/components/internal_temperature/internal_temperature.cpp Co-authored-by: Oxan van Leeuwen * Update internal_temperature.h * Remove unique_id * Update ESP32 variant detection --------- Co-authored-by: Oxan van Leeuwen --- CODEOWNERS | 1 + .../internal_temperature/__init__.py | 1 + .../internal_temperature.cpp | 58 +++++++++++++++++++ .../internal_temperature.h | 17 ++++++ .../components/internal_temperature/sensor.py | 31 ++++++++++ tests/test1.yaml | 2 + tests/test5.yaml | 2 + tests/test6.yaml | 4 ++ 8 files changed, 116 insertions(+) create mode 100644 esphome/components/internal_temperature/__init__.py create mode 100644 esphome/components/internal_temperature/internal_temperature.cpp create mode 100644 esphome/components/internal_temperature/internal_temperature.h create mode 100644 esphome/components/internal_temperature/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ca1da2f153..9cfa81b36c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -113,6 +113,7 @@ esphome/components/ina260/* @MrEditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate6/* @jesserockz esphome/components/integration/* @OttoWinter +esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion diff --git a/esphome/components/internal_temperature/__init__.py b/esphome/components/internal_temperature/__init__.py new file mode 100644 index 0000000000..9433ade13f --- /dev/null +++ b/esphome/components/internal_temperature/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Mat931"] diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp new file mode 100644 index 0000000000..9a22a77f63 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -0,0 +1,58 @@ +#include "internal_temperature.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 +#if defined(USE_ESP32_VARIANT_ESP32) +// there is no official API available on the original ESP32 +extern "C" { +uint8_t temprature_sens_read(); +} +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "driver/temp_sensor.h" +#endif // USE_ESP32_VARIANT +#endif // USE_ESP32 +#ifdef USE_RP2040 +#include "Arduino.h" +#endif // USE_RP2040 + +namespace esphome { +namespace internal_temperature { + +static const char *const TAG = "internal_temperature"; + +void InternalTemperatureSensor::update() { + float temperature = NAN; + bool success = false; +#ifdef USE_ESP32 +#if defined(USE_ESP32_VARIANT_ESP32) + uint8_t raw = temprature_sens_read(); + ESP_LOGV(TAG, "Raw temperature value: %d", raw); + temperature = (raw - 32) / 1.8f; + success = (raw != 128); +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); + temp_sensor_set_config(tsens); + temp_sensor_start(); + esp_err_t result = temp_sensor_read_celsius(&temperature); + temp_sensor_stop(); + success = (result == ESP_OK); +#endif // USE_ESP32_VARIANT +#endif // USE_ESP32 +#ifdef USE_RP2040 + temperature = analogReadTemp(); + success = (temperature != 0.0f); +#endif // USE_RP2040 + if (success && std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } + +} // namespace internal_temperature +} // namespace esphome diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h new file mode 100644 index 0000000000..0e46a69769 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature.h @@ -0,0 +1,17 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace internal_temperature { + +class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent { + public: + void dump_config() override; + + void update() override; +}; + +} // namespace internal_temperature +} // namespace esphome diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py new file mode 100644 index 0000000000..2655711bb5 --- /dev/null +++ b/esphome/components/internal_temperature/sensor.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature") +InternalTemperatureSensor = internal_temperature_ns.class_( + "InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + InternalTemperatureSensor, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ).extend(cv.polling_component_schema("60s")), + cv.only_on(["esp32", "rp2040"]), +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index a77f8802b9..bdc6da3406 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -324,6 +324,8 @@ mcp23s17: deviceaddress: 1 sensor: + - platform: internal_temperature + name: "Internal Temperature" - platform: ble_client type: characteristic ble_client_id: ble_foo diff --git a/tests/test5.yaml b/tests/test5.yaml index 5f72579d08..21419692e4 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -373,6 +373,8 @@ select: "Three": 3 sensor: + - platform: internal_temperature + name: "Internal Temperature" - platform: selec_meter total_active_energy: name: SelecEM2M Total Active Energy diff --git a/tests/test6.yaml b/tests/test6.yaml index 264773331e..2930400e34 100644 --- a/tests/test6.yaml +++ b/tests/test6.yaml @@ -37,3 +37,7 @@ switch: - platform: output output: pin_4 id: pin_4_switch + +sensor: + - platform: internal_temperature + name: "Internal Temperature" From 247916fe895ce6d3cb6a1973109d0667c25721d6 Mon Sep 17 00:00:00 2001 From: konsulten Date: Mon, 20 Feb 2023 03:48:59 +0100 Subject: [PATCH 043/115] BL0939 state_class set for energy sensors (#4463) BL0939 was missing TOTAL_INCREASING for energy (kWh) thus it did not show as statistics in home assistant --- esphome/components/bl0939/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/bl0939/sensor.py b/esphome/components/bl0939/sensor.py index bcc72ad61a..4c6e3ea4d9 100644 --- a/esphome/components/bl0939/sensor.py +++ b/esphome/components/bl0939/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_KILOWATT_HOURS, UNIT_VOLT, @@ -66,16 +67,19 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=3, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_ENERGY_2): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=3, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=3, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) From 50fbbf2d3ba32f696277529f1ea5413da330ede3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:22:37 +1300 Subject: [PATCH 044/115] Move remaining SENSOR_SCHEMA to use sensor_schema() (#4471) --- .../components/adc128s102/sensor/__init__.py | 17 +++++----- esphome/components/custom/sensor/__init__.py | 2 +- esphome/components/ezo/sensor.py | 4 +-- esphome/components/integration/sensor.py | 31 ++++++++++--------- .../components/kalman_combinator/sensor.py | 29 +++++++++-------- esphome/components/mcp3008/sensor.py | 19 +++++++----- esphome/components/mcp3204/sensor/__init__.py | 17 +++++----- .../modbus_controller/sensor/__init__.py | 4 +-- esphome/components/tuya/sensor/__init__.py | 17 +++++----- 9 files changed, 79 insertions(+), 61 deletions(-) diff --git a/esphome/components/adc128s102/sensor/__init__.py b/esphome/components/adc128s102/sensor/__init__.py index 3ab6fc4c38..640a1b628e 100644 --- a/esphome/components/adc128s102/sensor/__init__.py +++ b/esphome/components/adc128s102/sensor/__init__.py @@ -16,13 +16,16 @@ ADC128S102Sensor = adc128s102_ns.class_( ) CONF_ADC128S102_ID = "adc128s102_id" -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(ADC128S102Sensor), - cv.GenerateID(CONF_ADC128S102_ID): cv.use_id(ADC128S102), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), - } -).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA = ( + sensor.sensor_schema(ADC128S102Sensor) + .extend( + { + cv.GenerateID(CONF_ADC128S102_ID): cv.use_id(ADC128S102), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } + ) + .extend(cv.polling_component_schema("60s")) +) async def to_code(config): diff --git a/esphome/components/custom/sensor/__init__.py b/esphome/components/custom/sensor/__init__.py index bf9421e43e..be17d9a334 100644 --- a/esphome/components/custom/sensor/__init__.py +++ b/esphome/components/custom/sensor/__init__.py @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(CustomSensorConstructor), cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA), + cv.Required(CONF_SENSORS): cv.ensure_list(sensor.sensor_schema()), } ) diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index 049b1bf4d0..486ba0126e 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -41,9 +41,9 @@ DeviceInformationTrigger = ezo_ns.class_( 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/integration/sensor.py b/esphome/components/integration/sensor.py index 3d7cf03882..d57f909662 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -48,20 +48,23 @@ def inherit_accuracy_decimals(decimals, config): return decimals + 2 -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(IntegrationSensor), - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_TIME_UNIT): cv.enum(INTEGRATION_TIMES, lower=True), - cv.Optional(CONF_INTEGRATION_METHOD, default="trapezoid"): cv.enum( - INTEGRATION_METHODS, lower=True - ), - cv.Optional(CONF_RESTORE, default=False): cv.boolean, - cv.Optional("min_save_interval"): cv.invalid( - "min_save_interval was removed in 2022.8.0. Please use the `preferences` -> `flash_write_interval` to adjust." - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema(IntegrationSensor) + .extend( + { + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_TIME_UNIT): cv.enum(INTEGRATION_TIMES, lower=True), + cv.Optional(CONF_INTEGRATION_METHOD, default="trapezoid"): cv.enum( + INTEGRATION_METHODS, lower=True + ), + cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional("min_save_interval"): cv.invalid( + "min_save_interval was removed in 2022.8.0. Please use the `preferences` -> `flash_write_interval` to adjust." + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py index 9223f883b2..28b96077cc 100644 --- a/esphome/components/kalman_combinator/sensor.py +++ b/esphome/components/kalman_combinator/sensor.py @@ -23,20 +23,23 @@ CONF_PROCESS_STD_DEV = "process_std_dev" CONF_STD_DEV = "std_dev" -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(KalmanCombinatorComponent), - cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, - cv.Required(CONF_SOURCES): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), - } +CONFIG_SCHEMA = ( + sensor.sensor_schema(KalmanCombinatorComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, + cv.Required(CONF_SOURCES): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), + } + ), ), - ), - cv.Optional(CONF_STD_DEV): sensor.SENSOR_SCHEMA, - } + cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), + } + ) ) # Inherit some sensor values from the first source, for both the state and the error value diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py index d4b9e979ce..dd5141484b 100644 --- a/esphome/components/mcp3008/sensor.py +++ b/esphome/components/mcp3008/sensor.py @@ -14,14 +14,17 @@ MCP3008Sensor = mcp3008_ns.class_( CONF_REFERENCE_VOLTAGE = "reference_voltage" CONF_MCP3008_ID = "mcp3008_id" -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MCP3008Sensor), - cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, - } -).extend(cv.polling_component_schema("1s")) +CONFIG_SCHEMA = ( + sensor.sensor_schema(MCP3008Sensor) + .extend( + { + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("1s")) +) async def to_code(config): diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index 404880d405..6a81c6ec84 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -13,13 +13,16 @@ MCP3204Sensor = mcp3204_ns.class_( ) CONF_MCP3204_ID = "mcp3204_id" -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MCP3204Sensor), - cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), - } -).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA = ( + sensor.sensor_schema(MCP3204Sensor) + .extend( + { + cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + } + ) + .extend(cv.polling_component_schema("60s")) +) async def to_code(config): diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index da7b8928b4..0e4588cfef 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -32,11 +32,11 @@ ModbusSensor = modbus_controller_ns.class_( ) CONFIG_SCHEMA = cv.All( - sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + sensor.sensor_schema(ModbusSensor) + .extend(cv.COMPONENT_SCHEMA) .extend(ModbusItemBaseSchema) .extend( { - cv.GenerateID(): cv.declare_id(ModbusSensor), cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py index 441400fa43..69711204a8 100644 --- a/esphome/components/tuya/sensor/__init__.py +++ b/esphome/components/tuya/sensor/__init__.py @@ -9,13 +9,16 @@ CODEOWNERS = ["@jesserockz"] TuyaSensor = tuya_ns.class_("TuyaSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TuyaSensor), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema(TuyaSensor) + .extend( + { + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): From 0e1d018ce3f51f555e71ef14ba50c5244218d5f8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Feb 2023 11:22:43 +1300 Subject: [PATCH 045/115] Allow specifying target and current visual steps for climate (#4440) * Allow specifying target and current visual steps for climate * Fixes * format * format --- esphome/components/api/api.proto | 3 +- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_pb2.cpp | 18 ++++++-- esphome/components/api/api_pb2.h | 3 +- esphome/components/climate/__init__.py | 41 +++++++++++++++++-- esphome/components/climate/climate.cpp | 15 ++++--- esphome/components/climate/climate.h | 5 ++- esphome/components/climate/climate_traits.cpp | 8 +++- esphome/components/climate/climate_traits.h | 20 +++++++-- esphome/components/mqtt/mqtt_climate.cpp | 13 +++--- esphome/components/web_server/web_server.cpp | 21 +++++----- 11 files changed, 111 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index ffb3bcb07e..eb639f2065 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -829,7 +829,7 @@ message ListEntitiesClimateResponse { repeated ClimateMode supported_modes = 7; float visual_min_temperature = 8; float visual_max_temperature = 9; - float visual_temperature_step = 10; + float visual_target_temperature_step = 10; // for older peer versions - in new system this // is if CLIMATE_PRESET_AWAY exists is supported_presets bool legacy_supports_away = 11; @@ -842,6 +842,7 @@ message ListEntitiesClimateResponse { bool disabled_by_default = 18; string icon = 19; EntityCategory entity_category = 20; + float visual_current_temperature_step = 21; } message ClimateStateResponse { option (id) = 47; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 65659941d6..487aa53193 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -548,7 +548,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); - msg.visual_temperature_step = traits.get_visual_temperature_step(); + msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); + msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); + msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9df05d2978..3fc1bfa95d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3451,7 +3451,11 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val return true; } case 10: { - this->visual_temperature_step = value.as_float(); + this->visual_target_temperature_step = value.as_float(); + return true; + } + case 21: { + this->visual_current_temperature_step = value.as_float(); return true; } default: @@ -3470,7 +3474,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); - buffer.encode_float(10, this->visual_temperature_step); + buffer.encode_float(10, this->visual_target_temperature_step); buffer.encode_bool(11, this->legacy_supports_away); buffer.encode_bool(12, this->supports_action); for (auto &it : this->supported_fan_modes) { @@ -3491,6 +3495,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(18, this->disabled_by_default); buffer.encode_string(19, this->icon); buffer.encode_enum(20, this->entity_category); + buffer.encode_float(21, this->visual_current_temperature_step); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3537,8 +3542,8 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" visual_temperature_step: "); - sprintf(buffer, "%g", this->visual_temperature_step); + out.append(" visual_target_temperature_step: "); + sprintf(buffer, "%g", this->visual_target_temperature_step); out.append(buffer); out.append("\n"); @@ -3591,6 +3596,11 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" visual_current_temperature_step: "); + sprintf(buffer, "%g", this->visual_current_temperature_step); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2db1c6fafa..e192892e72 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -915,7 +915,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::vector supported_modes{}; float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; - float visual_temperature_step{0.0f}; + float visual_target_temperature_step{0.0f}; bool legacy_supports_away{false}; bool supports_action{false}; std::vector supported_fan_modes{}; @@ -926,6 +926,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + float visual_current_temperature_step{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index eaa87afcb1..709d0d12ed 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -104,10 +104,40 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) +CONF_CURRENT_TEMPERATURE = "current_temperature" + +visual_temperature = cv.float_with_unit( + "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" +) + + +def single_visual_temperature(value): + if isinstance(value, dict): + return value + + value = visual_temperature(value) + return VISUAL_TEMPERATURE_STEP_SCHEMA( + { + CONF_TARGET_TEMPERATURE: value, + CONF_CURRENT_TEMPERATURE: value, + } + ) + + # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) +VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any( + single_visual_temperature, + cv.Schema( + { + cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, + cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, + } + ), +) + CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(Climate), @@ -116,9 +146,7 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). { cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, - cv.Optional(CONF_TEMPERATURE_STEP): cv.float_with_unit( - "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" - ), + cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, } ), cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( @@ -193,7 +221,12 @@ async def setup_climate_core_(var, config): if CONF_MAX_TEMPERATURE in visual: cg.add(var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE])) if CONF_TEMPERATURE_STEP in visual: - cg.add(var.set_visual_temperature_step_override(visual[CONF_TEMPERATURE_STEP])) + cg.add( + var.set_visual_temperature_step_override( + visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE], + visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], + ) + ) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index e1611d2fa9..b80fe640c8 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -430,9 +430,11 @@ ClimateTraits Climate::get_traits() { if (this->visual_max_temperature_override_.has_value()) { traits.set_visual_max_temperature(*this->visual_max_temperature_override_); } - if (this->visual_temperature_step_override_.has_value()) { - traits.set_visual_temperature_step(*this->visual_temperature_step_override_); + if (this->visual_target_temperature_step_override_.has_value()) { + traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); + traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); } + return traits; } @@ -442,8 +444,9 @@ void Climate::set_visual_min_temperature_override(float visual_min_temperature_o void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { this->visual_max_temperature_override_ = visual_max_temperature_override; } -void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) { - this->visual_temperature_step_override_ = visual_temperature_step_override; +void Climate::set_visual_temperature_step_override(float target, float current) { + this->visual_target_temperature_step_override_ = target; + this->visual_current_temperature_step_override_ = current; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" @@ -541,7 +544,9 @@ void Climate::dump_traits_(const char *tag) { ESP_LOGCONFIG(tag, " [x] Visual settings:"); ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step()); + ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); + ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); if (traits.get_supports_current_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports current temperature"); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index d508bb31b0..8cc260abbe 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -241,7 +241,7 @@ class Climate : public EntityBase { void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); - void set_visual_temperature_step_override(float visual_temperature_step_override); + void set_visual_temperature_step_override(float target, float current); protected: friend ClimateCall; @@ -288,7 +288,8 @@ class Climate : public EntityBase { ESPPreferenceObject rtc_; optional visual_min_temperature_override_{}; optional visual_max_temperature_override_{}; - optional visual_temperature_step_override_{}; + optional visual_target_temperature_step_override_{}; + optional visual_current_temperature_step_override_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 38ded6cdf7..342dffaad6 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -3,8 +3,12 @@ namespace esphome { namespace climate { -int8_t ClimateTraits::get_temperature_accuracy_decimals() const { - return step_to_accuracy_decimals(this->visual_temperature_step_); +int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { + return step_to_accuracy_decimals(this->visual_target_temperature_step_); +} + +int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const { + return step_to_accuracy_decimals(this->visual_current_temperature_step_); } } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 9da9bb7374..ffbd8c5ae0 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -147,9 +147,20 @@ class ClimateTraits { void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } float get_visual_max_temperature() const { return visual_max_temperature_; } void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } - float get_visual_temperature_step() const { return visual_temperature_step_; } - int8_t get_temperature_accuracy_decimals() const; - void set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } + float get_visual_target_temperature_step() const { return visual_target_temperature_step_; } + float get_visual_current_temperature_step() const { return visual_current_temperature_step_; } + void set_visual_target_temperature_step(float temperature_step) { + visual_target_temperature_step_ = temperature_step; + } + void set_visual_current_temperature_step(float temperature_step) { + visual_current_temperature_step_ = temperature_step; + } + void set_visual_temperature_step(float temperature_step) { + visual_target_temperature_step_ = temperature_step; + visual_current_temperature_step_ = temperature_step; + } + int8_t get_target_temperature_accuracy_decimals() const; + int8_t get_current_temperature_accuracy_decimals() const; protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { @@ -186,7 +197,8 @@ class ClimateTraits { float visual_min_temperature_{10}; float visual_max_temperature_{30}; - float visual_temperature_step_{0.1}; + float visual_target_temperature_step_{0.1}; + float visual_current_temperature_step_{0.1}; }; } // namespace climate diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 1947e02b9e..e88ffcc37c 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -62,7 +62,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // max_temp root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); // temp_step - root["temp_step"] = traits.get_visual_temperature_step(); + root["temp_step"] = traits.get_visual_target_temperature_step(); // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; @@ -281,21 +281,22 @@ bool MQTTClimateComponent::publish_state_() { bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) success = false; - int8_t accuracy = traits.get_temperature_accuracy_decimals(); + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) { - std::string payload = value_accuracy_to_string(this->device_->current_temperature, accuracy); + std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy); if (!this->publish(this->get_current_temperature_state_topic(), payload)) success = false; } if (traits.get_supports_two_point_target_temperature()) { - std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, accuracy); + std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy); if (!this->publish(this->get_target_temperature_low_state_topic(), payload)) success = false; - payload = value_accuracy_to_string(this->device_->target_temperature_high, accuracy); + payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy); if (!this->publish(this->get_target_temperature_high_state_topic(), payload)) success = false; } else { - std::string payload = value_accuracy_to_string(this->device_->target_temperature, accuracy); + std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy); if (!this->publish(this->get_target_temperature_state_topic(), payload)) success = false; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 513399e257..6c74c79ce6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -873,7 +873,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf return json::build_json([obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); - int8_t accuracy = traits.get_temperature_accuracy_decimals(); + int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); + int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); char __buf[16]; if (start_config == DETAIL_ALL) { @@ -910,9 +911,9 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf bool has_state = false; root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), accuracy); - root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), accuracy); - root["step"] = traits.get_visual_temperature_step(); + root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); + root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root["step"] = traits.get_visual_target_temperature_step(); if (traits.get_supports_action()) { root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); root["state"] = root["action"]; @@ -935,20 +936,20 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf } if (traits.get_supports_current_temperature()) { if (!std::isnan(obj->current_temperature)) { - root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, accuracy); + root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy); } else { root["current_temperature"] = "NA"; } } if (traits.get_supports_two_point_target_temperature()) { - root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, accuracy); - root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, accuracy); + root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); if (!has_state) { - root["state"] = - value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, accuracy); + root["state"] = value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy); } } else { - root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, accuracy); + root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, target_accuracy); if (!has_state) root["state"] = root["target_temperature"]; } From 48e76e15387ed6e4ea35c51b5b294c3bd6277694 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 11:29:20 +1300 Subject: [PATCH 046/115] Bump aioesphomeapi from 13.3.1 to 13.4.0 (#4472) Bumps [aioesphomeapi](https://github.com/esphome/aioesphomeapi) from 13.3.1 to 13.4.0. - [Release notes](https://github.com/esphome/aioesphomeapi/releases) - [Commits](https://github.com/esphome/aioesphomeapi/compare/v13.3.1...v13.4.0) --- updated-dependencies: - dependency-name: aioesphomeapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c2490f36a4..2c64639fd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.6 # When updating platformio, also update Dockerfile esptool==4.4 click==8.1.3 esphome-dashboard==20230214.0 -aioesphomeapi==13.3.1 +aioesphomeapi==13.4.0 zeroconf==0.47.3 # esp-idf requires this, but doesn't bundle it by default From ba6f89a757fdafa0b0b21a782e2840fe94048154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 20 Feb 2023 23:31:25 +0100 Subject: [PATCH 047/115] toshiba: add support for quiet fan mode (#4283) --- esphome/components/toshiba/toshiba.cpp | 28 ++++++++++++++++++++++++++ esphome/components/toshiba/toshiba.h | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index a070ccceb2..33d36d6a69 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -190,6 +190,10 @@ void ToshibaClimate::transmit_generic_() { uint8_t fan; switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_QUIET: + fan = TOSHIBA_FAN_SPEED_QUIET; + break; + case climate::CLIMATE_FAN_LOW: fan = TOSHIBA_FAN_SPEED_1; break; @@ -692,6 +696,30 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_HEAT_COOL; } + // Get the fan mode + switch (message[6] & 0xF0) { + case TOSHIBA_FAN_SPEED_QUIET: + this->fan_mode = climate::CLIMATE_FAN_QUIET; + break; + + case TOSHIBA_FAN_SPEED_1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + + case TOSHIBA_FAN_SPEED_3: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + + case TOSHIBA_FAN_SPEED_5: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + + case TOSHIBA_FAN_SPEED_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + // Get the target temperature this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN; } diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index 729548e747..83e85c34db 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -25,7 +25,7 @@ class ToshibaClimate : public climate_ir::ClimateIR { ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_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}) {} void setup() override; void set_model(Model model) { this->model_ = model; } From 8fb481751f08df357dc4b0ab04cbe9b8164f0350 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:01:49 +1300 Subject: [PATCH 048/115] Bump esptool from 4.4 to 4.5 (#4428) Bumps [esptool](https://github.com/espressif/esptool) from 4.4 to 4.5. - [Release notes](https://github.com/espressif/esptool/releases) - [Commits](https://github.com/espressif/esptool/compare/v4.4...v4.5) --- updated-dependencies: - dependency-name: esptool dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c64639fd2..bfad2433c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.6 # When updating platformio, also update Dockerfile -esptool==4.4 +esptool==4.5 click==8.1.3 esphome-dashboard==20230214.0 aioesphomeapi==13.4.0 From d16eff5039a7503cc8274464290c7a0f1eec47b2 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 21 Feb 2023 22:48:29 +0100 Subject: [PATCH 049/115] Support Mopeka Standard LPG tank bluetooth sensor (#4351) * Add mopeka standard tank sensor. * Enhance mopeka ble to find standard sensors. * Updated `CODEOWNERS` file * Move default from cpp to py. * Format documents with esphome settings. * Linter wants changes. * Update name of `get_lpg_speed_of_sound`. * manually update `CODEOWNERS`. * Manually update CODEOWNER, because `build_codeowners.py. is failing. * Add comments. * Use percentage for `propane_butane_mix`. * add config to `dump_config()` * Formatting * Use struct for data parsing and find best data. * Add `this`. * Consistant naming of configuration. * Fix format issues. * Make clang-tidy happy. * Adjust loop variable. --------- Co-authored-by: Your Name --- CODEOWNERS | 3 +- esphome/components/mopeka_ble/__init__.py | 9 +- esphome/components/mopeka_ble/mopeka_ble.cpp | 77 ++++-- esphome/components/mopeka_ble/mopeka_ble.h | 11 +- .../components/mopeka_std_check/__init__.py | 1 + .../mopeka_std_check/mopeka_std_check.cpp | 226 ++++++++++++++++++ .../mopeka_std_check/mopeka_std_check.h | 78 ++++++ esphome/components/mopeka_std_check/sensor.py | 139 +++++++++++ 8 files changed, 520 insertions(+), 24 deletions(-) create mode 100644 esphome/components/mopeka_std_check/__init__.py create mode 100644 esphome/components/mopeka_std_check/mopeka_std_check.cpp create mode 100644 esphome/components/mopeka_std_check/mopeka_std_check.h create mode 100644 esphome/components/mopeka_std_check/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9cfa81b36c..79c9c4f94b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -161,8 +161,9 @@ esphome/components/modbus_controller/select/* @martgras @stegm esphome/components/modbus_controller/sensor/* @martgras esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras -esphome/components/mopeka_ble/* @spbrogan +esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan esphome/components/mopeka_pro_check/* @spbrogan +esphome/components/mopeka_std_check/* @Fabian-Schmidt esphome/components/mpl3115a2/* @kbickar esphome/components/mpu6886/* @fabaff esphome/components/network/* @esphome/core diff --git a/esphome/components/mopeka_ble/__init__.py b/esphome/components/mopeka_ble/__init__.py index 47396435a8..c89eae7933 100644 --- a/esphome/components/mopeka_ble/__init__.py +++ b/esphome/components/mopeka_ble/__init__.py @@ -3,9 +3,11 @@ import esphome.config_validation as cv from esphome.components import esp32_ble_tracker from esphome.const import CONF_ID -CODEOWNERS = ["@spbrogan"] +CODEOWNERS = ["@spbrogan", "@Fabian-Schmidt"] DEPENDENCIES = ["esp32_ble_tracker"] +CONF_SHOW_SENSORS_WITHOUT_SYNC = "show_sensors_without_sync" + mopeka_ble_ns = cg.esphome_ns.namespace("mopeka_ble") MopekaListener = mopeka_ble_ns.class_( "MopekaListener", esp32_ble_tracker.ESPBTDeviceListener @@ -14,10 +16,15 @@ MopekaListener = mopeka_ble_ns.class_( CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(MopekaListener), + cv.Optional(CONF_SHOW_SENSORS_WITHOUT_SYNC, default=False): cv.boolean, } ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + if CONF_SHOW_SENSORS_WITHOUT_SYNC in config: + cg.add( + var.set_show_sensors_without_sync(config[CONF_SHOW_SENSORS_WITHOUT_SYNC]) + ) await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/mopeka_ble/mopeka_ble.cpp b/esphome/components/mopeka_ble/mopeka_ble.cpp index 844d3a7dfd..07c8ac5d71 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.cpp +++ b/esphome/components/mopeka_ble/mopeka_ble.cpp @@ -1,4 +1,5 @@ #include "mopeka_ble.h" + #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -7,43 +8,83 @@ namespace esphome { namespace mopeka_ble { static const char *const TAG = "mopeka_ble"; -static const uint8_t MANUFACTURER_DATA_LENGTH = 10; -static const uint16_t MANUFACTURER_ID = 0x0059; + +// Mopeka Std (CC2540) sensor details +static const uint16_t SERVICE_UUID_CC2540 = 0xADA0; +static const uint16_t MANUFACTURER_CC2540_ID = 0x000D; // Texas Instruments (TI) +static const uint8_t MANUFACTURER_CC2540_DATA_LENGTH = 23; + +// Mopeka Pro (NRF52) sensor details +static const uint16_t SERVICE_UUID_NRF52 = 0xFEE5; +static const uint16_t MANUFACTURER_NRF52_ID = 0x0059; // Nordic +static const uint8_t MANUFACTURER_NRF52_DATA_LENGTH = 10; /** * Parse all incoming BLE payloads to see if it is a Mopeka BLE advertisement. * Currently this supports the following products: * - * Mopeka Pro Check. - * If the sync button is pressed, report the MAC so a user can add this as a sensor. + * - Mopeka Std Check - uses the chip CC2540 by Texas Instruments (TI) + * - Mopeka Pro Check - uses the chip NRF52 by Nordic + * + * If the sync button is pressed, report the MAC so a user can add this as a sensor. Or if user has configured + * `show_sensors_without_sync_` than report all visible sensors. + * Three points are used to identify a sensor: + * + * - Bluetooth service uuid + * - Bluetooth manufacturer id + * - Bluetooth data frame size */ bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - const auto &manu_datas = device.get_manufacturer_datas(); + // Fetch information about BLE device. + const auto &service_uuids = device.get_service_uuids(); + if (service_uuids.size() != 1) { + return false; + } + const auto &service_uuid = service_uuids[0]; + const auto &manu_datas = device.get_manufacturer_datas(); if (manu_datas.size() != 1) { return false; } - const auto &manu_data = manu_datas[0]; - if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { - return false; + // Is the device maybe a Mopeka Std (CC2540) sensor. + if (service_uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(SERVICE_UUID_CC2540)) { + if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_CC2540_ID)) { + return false; + } + + if (manu_data.data.size() != MANUFACTURER_CC2540_DATA_LENGTH) { + return false; + } + + const bool sync_button_pressed = (manu_data.data[3] & 0x80) != 0; + + if (this->show_sensors_without_sync_ || sync_button_pressed) { + ESP_LOGI(TAG, "MOPEKA STD (CC2540) SENSOR FOUND: %s", device.address_str().c_str()); + } + + // Is the device maybe a Mopeka Pro (NRF52) sensor. + } else if (service_uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(SERVICE_UUID_NRF52)) { + if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_NRF52_ID)) { + return false; + } + + if (manu_data.data.size() != MANUFACTURER_NRF52_DATA_LENGTH) { + return false; + } + + const bool sync_button_pressed = (manu_data.data[2] & 0x80) != 0; + + if (this->show_sensors_without_sync_ || sync_button_pressed) { + ESP_LOGI(TAG, "MOPEKA PRO (NRF52) SENSOR FOUND: %s", device.address_str().c_str()); + } } - if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_ID)) { - return false; - } - - if (this->parse_sync_button_(manu_data.data)) { - // button pressed - ESP_LOGI(TAG, "SENSOR FOUND: %s", device.address_str().c_str()); - } return false; } -bool MopekaListener::parse_sync_button_(const std::vector &message) { return (message[2] & 0x80) != 0; } - } // namespace mopeka_ble } // namespace esphome diff --git a/esphome/components/mopeka_ble/mopeka_ble.h b/esphome/components/mopeka_ble/mopeka_ble.h index f88bad4f3a..b7d0c5a9c5 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.h +++ b/esphome/components/mopeka_ble/mopeka_ble.h @@ -1,10 +1,10 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" - #include +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" + #ifdef USE_ESP32 namespace esphome { @@ -13,9 +13,12 @@ namespace mopeka_ble { class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void set_show_sensors_without_sync(bool show_sensors_without_sync) { + show_sensors_without_sync_ = show_sensors_without_sync; + } protected: - bool parse_sync_button_(const std::vector &message); + bool show_sensors_without_sync_; }; } // namespace mopeka_ble diff --git a/esphome/components/mopeka_std_check/__init__.py b/esphome/components/mopeka_std_check/__init__.py new file mode 100644 index 0000000000..88e344464f --- /dev/null +++ b/esphome/components/mopeka_std_check/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Fabian-Schmidt"] diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp new file mode 100644 index 0000000000..cbe51b8f2d --- /dev/null +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -0,0 +1,226 @@ +#include "mopeka_std_check.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_std_check { + +static const char *const TAG = "mopeka_std_check"; +static const uint16_t SERVICE_UUID = 0xADA0; +static const uint8_t MANUFACTURER_DATA_LENGTH = 23; +static const uint16_t MANUFACTURER_ID = 0x000D; + +void MopekaStdCheck::dump_config() { + ESP_LOGCONFIG(TAG, "Mopeka Std Check"); + ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100); + ESP_LOGCONFIG(TAG, " Tank distance empty: %imm", this->empty_mm_); + ESP_LOGCONFIG(TAG, " Tank distance full: %imm", this->full_mm_); + LOG_SENSOR(" ", "Level", this->level_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Reading Distance", this->distance_); +} + +/** + * Main parse function that gets called for all ble advertisements. + * Check if advertisement is for our sensor and if so decode it and + * update the sensor state data. + */ +bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + { + // Validate address. + if (device.address_uint64() != this->address_) { + return false; + } + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + } + + { + // Validate service uuid + const auto &service_uuids = device.get_service_uuids(); + if (service_uuids.size() != 1) { + return false; + } + const auto &service_uuid = service_uuids[0]; + if (service_uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(SERVICE_UUID)) { + return false; + } + } + + const auto &manu_datas = device.get_manufacturer_datas(); + + if (manu_datas.size() != 1) { + ESP_LOGE(TAG, "%s: Unexpected manu_datas size (%d)", device.address_str().c_str(), manu_datas.size()); + return false; + } + + const auto &manu_data = manu_datas[0]; + + ESP_LOGVV(TAG, "%s: Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); + + if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { + ESP_LOGE(TAG, "%s: Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size()); + return false; + } + + // Now parse the data + const auto *mopeka_data = (const mopeka_std_package *) manu_data.data.data(); + + const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; + if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL) { + ESP_LOGE(TAG, "%s: Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); + return false; + } + + ESP_LOGVV(TAG, "%s: Sensor slow update rate: %d", device.address_str().c_str(), mopeka_data->slow_update_rate); + ESP_LOGVV(TAG, "%s: Sensor sync pressed: %d", device.address_str().c_str(), mopeka_data->sync_pressed); + for (u_int8_t i = 0; i < 3; i++) { + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 1, + mopeka_data->val[i].value_0, mopeka_data->val[i].time_0); + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 2, + mopeka_data->val[i].value_1, mopeka_data->val[i].time_1); + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 3, + mopeka_data->val[i].value_2, mopeka_data->val[i].time_2); + ESP_LOGVV(TAG, "%s: %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 4, + mopeka_data->val[i].value_3, mopeka_data->val[i].time_3); + } + + // Get battery level first + if (this->battery_level_ != nullptr) { + uint8_t level = this->parse_battery_level_(mopeka_data); + this->battery_level_->publish_state(level); + } + + // Get temperature of sensor + uint8_t temp_in_c = this->parse_temperature_(mopeka_data); + if (this->temperature_ != nullptr) { + this->temperature_->publish_state(temp_in_c); + } + + // Get distance and level if either are sensors + if ((this->distance_ != nullptr) || (this->level_ != nullptr)) { + // Message contains 12 sensor dataset each 10 bytes long. + // each sensor dataset contains 5 byte time and 5 byte value. + + // time in 10us ticks. + // value is amplitude. + + std::array measurements_time = {}; + std::array measurements_value = {}; + // Copy measurements over into my array. + { + u_int8_t measurements_index = 0; + for (u_int8_t i = 0; i < 3; i++) { + measurements_time[measurements_index] = mopeka_data->val[i].time_0 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_0; + measurements_index++; + measurements_time[measurements_index] = mopeka_data->val[i].time_1 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_1; + measurements_index++; + measurements_time[measurements_index] = mopeka_data->val[i].time_2 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_2; + measurements_index++; + measurements_time[measurements_index] = mopeka_data->val[i].time_3 + 1; + measurements_value[measurements_index] = mopeka_data->val[i].value_3; + measurements_index++; + } + } + + // Find best(strongest) value(amplitude) and it's belonging time in sensor dataset. + u_int8_t number_of_usable_values = 0; + u_int16_t best_value = 0; + u_int16_t best_time = 0; + { + u_int16_t measurement_time = 0; + for (u_int8_t i = 0; i < 12; i++) { + // Time is summed up until a value is reported. This allows time values larger than the 5 bits in transport. + measurement_time += measurements_time[i]; + if (measurements_value[i] != 0) { + // I got a value + number_of_usable_values++; + if (measurements_value[i] > best_value) { + // This value is better than a previous one. + best_value = measurements_value[i]; + best_time = measurement_time; + // Reset measurement_time or next values. + measurement_time = 0; + } + } + } + } + + ESP_LOGV(TAG, "%s: Found %u values with best data %u time %u.", device.address_str().c_str(), + number_of_usable_values, best_value, best_time); + + if (number_of_usable_values < 2 || best_value < 2 || best_time < 2) { + // At least two measurement values must be present. + ESP_LOGW(TAG, "%s: Poor read quality. Setting distance to 0.", device.address_str().c_str()); + if (this->distance_ != nullptr) { + this->distance_->publish_state(0); + } + if (this->level_ != nullptr) { + this->level_->publish_state(0); + } + } else { + float lpg_speed_of_sound = this->get_lpg_speed_of_sound_(temp_in_c); + ESP_LOGV(TAG, "%s: Speed of sound in current fluid %f m/s", device.address_str().c_str(), lpg_speed_of_sound); + + uint32_t distance_value = lpg_speed_of_sound * best_time / 100.0f; + + // update distance sensor + if (this->distance_ != nullptr) { + this->distance_->publish_state(distance_value); + } + + // update level sensor + if (this->level_ != nullptr) { + uint8_t tank_level = 0; + if (distance_value >= this->full_mm_) { + tank_level = 100; // cap at 100% + } else if (distance_value > this->empty_mm_) { + tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_)); + } + this->level_->publish_state(tank_level); + } + } + } + + return true; +} + +float MopekaStdCheck::get_lpg_speed_of_sound_(float temperature) { + return 1040.71f - 4.87f * temperature - 137.5f * this->propane_butane_mix_ - 0.0107f * temperature * temperature - + 1.63f * temperature * this->propane_butane_mix_; +} + +uint8_t MopekaStdCheck::parse_battery_level_(const mopeka_std_package *message) { + const float voltage = (float) ((message->raw_voltage / 256.0f) * 2.0f + 1.5f); + ESP_LOGVV(TAG, "Sensor battery voltage: %f V", voltage); + // convert voltage and scale for CR2032 + const float percent = (voltage - 2.2f) / 0.65f * 100.0f; + if (percent < 0.0f) { + return 0; + } + if (percent > 100.0f) { + return 100; + } + return (uint8_t) percent; +} + +uint8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { + uint8_t tmp = message->raw_temp; + if (tmp == 0x0) { + return -40; + } else { + return (uint8_t)((tmp - 25.0f) * 1.776964f); + } +} + +} // namespace mopeka_std_check +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h new file mode 100644 index 0000000000..e4d81afbd7 --- /dev/null +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_std_check { + +enum SensorType { + STANDARD = 0x02, + XL = 0x03, +}; + +// 4 values in one struct so it aligns to 8 byte. One `mopeka_std_values` is 40 bit long. +struct mopeka_std_values { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + u_int16_t time_0 : 5; + u_int16_t value_0 : 5; + u_int16_t time_1 : 5; + u_int16_t value_1 : 5; + u_int16_t time_2 : 5; + u_int16_t value_2 : 5; + u_int16_t time_3 : 5; + u_int16_t value_3 : 5; +} __attribute__((packed)); + +struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + u_int8_t data_0 : 8; + u_int8_t data_1 : 8; + u_int8_t raw_voltage : 8; + + u_int8_t raw_temp : 6; + bool slow_update_rate : 1; + bool sync_pressed : 1; + + mopeka_std_values val[4]; +} __attribute__((packed)); + +class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_level(sensor::Sensor *level) { this->level_ = level; }; + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }; + void set_battery_level(sensor::Sensor *bat) { this->battery_level_ = bat; }; + void set_distance(sensor::Sensor *distance) { this->distance_ = distance; }; + void set_propane_butane_mix(float val) { this->propane_butane_mix_ = val; }; + void set_tank_full(float full) { this->full_mm_ = full; }; + void set_tank_empty(float empty) { this->empty_mm_ = empty; }; + + protected: + uint64_t address_; + sensor::Sensor *level_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *distance_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + float propane_butane_mix_; + uint32_t full_mm_; + uint32_t empty_mm_; + + float get_lpg_speed_of_sound_(float temperature); + uint8_t parse_battery_level_(const mopeka_std_package *message); + uint8_t parse_temperature_(const mopeka_std_package *message); +}; + +} // namespace mopeka_std_check +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_std_check/sensor.py b/esphome/components/mopeka_std_check/sensor.py new file mode 100644 index 0000000000..bbba798e95 --- /dev/null +++ b/esphome/components/mopeka_std_check/sensor.py @@ -0,0 +1,139 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_DISTANCE, + CONF_MAC_ADDRESS, + CONF_ID, + ICON_THERMOMETER, + ICON_RULER, + UNIT_PERCENT, + CONF_LEVEL, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + STATE_CLASS_MEASUREMENT, + CONF_BATTERY_LEVEL, + DEVICE_CLASS_BATTERY, +) + +CONF_TANK_TYPE = "tank_type" +CONF_CUSTOM_DISTANCE_FULL = "custom_distance_full" +CONF_CUSTOM_DISTANCE_EMPTY = "custom_distance_empty" +CONF_PROPANE_BUTANE_MIX = "propane_butane_mix" + +ICON_PROPANE_TANK = "mdi:propane-tank" + +TANK_TYPE_CUSTOM = "CUSTOM" + +UNIT_MILLIMETER = "mm" + + +def small_distance(value): + """small_distance is stored in mm""" + meters = cv.distance(value) + return meters * 1000 + + +# +# Map of standard tank types to their +# empty and full distance values. +# Format is - tank name: (empty distance in mm, full distance in mm) +# +CONF_SUPPORTED_TANKS_MAP = { + TANK_TYPE_CUSTOM: (38, 100), + "NORTH_AMERICA_20LB_VERTICAL": (38, 254), # empty/full readings for 20lb US tank + "NORTH_AMERICA_30LB_VERTICAL": (38, 381), + "NORTH_AMERICA_40LB_VERTICAL": (38, 508), + "EUROPE_6KG": (38, 336), + "EUROPE_11KG": (38, 366), + "EUROPE_14KG": (38, 467), +} + +CODEOWNERS = ["@Fabian-Schmidt"] +DEPENDENCIES = ["esp32_ble_tracker"] + +mopeka_std_check_ns = cg.esphome_ns.namespace("mopeka_std_check") +MopekaStdCheck = mopeka_std_check_ns.class_( + "MopekaStdCheck", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MopekaStdCheck), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance, + cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance, + cv.Optional(CONF_PROPANE_BUTANE_MIX, default="100%"): cv.percentage, + cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True), + cv.Optional(CONF_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, + ), + cv.Optional(CONF_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PROPANE_TANK, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if config[CONF_TANK_TYPE] == TANK_TYPE_CUSTOM: + # Support custom tank min/max + if CONF_CUSTOM_DISTANCE_EMPTY in config: + cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY])) + else: + cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0])) + if CONF_CUSTOM_DISTANCE_FULL in config: + cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL])) + else: + cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1])) + else: + # Set the Tank empty and full based on map - User is requesting standard tank + t = config[CONF_TANK_TYPE] + cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0])) + cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1])) + + if CONF_PROPANE_BUTANE_MIX in config: + cg.add(var.set_propane_butane_mix(config[CONF_PROPANE_BUTANE_MIX])) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_LEVEL]) + cg.add(var.set_level(sens)) + if CONF_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_DISTANCE]) + cg.add(var.set_distance(sens)) + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) From 38a01988a533c526196f06f7c72fb72205abccb3 Mon Sep 17 00:00:00 2001 From: Mikhail Zakharov Date: Tue, 21 Feb 2023 16:52:06 -0500 Subject: [PATCH 050/115] fix library override logic (#4474) * fix library override logic * formatting --- esphome/core/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 545fae381f..117b19a6ae 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -653,7 +653,15 @@ class EsphomeCore: f"Library {library} must be instance of Library, not {type(library)}" ) for other in self.libraries[:]: - if other.name != library.name or other.name is None or library.name is None: + if other.name is None or library.name is None: + continue + library_name = ( + library.name if "/" not in library.name else library.name.split("/")[1] + ) + other_name = ( + other.name if "/" not in other.name else other.name.split("/")[1] + ) + if other_name != library_name: continue if other.repository is not None: if library.repository is None or other.repository == library.repository: From 98b3d294aab333e73c92117bbb38e25969d73c6c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Feb 2023 19:47:50 -0600 Subject: [PATCH 051/115] Sprinkler "v2" updates (#4159) * Add standby switch * Add support for arbitrary run duration in start_single_valve action * Add divider feature * Allow zero multiplier * Fixes for #3740, misc. cleanup and polishing * Integrate number components for multiplier, repeat and run duration * Add various methods to get time remaining * Add next_prev_ignore_disabled flag * Optimize next/previous valve selection methods * Add numbers_use_minutes flag * Initialize switch states as they are set up * Ensure SprinklerControllerSwitch has state if it's not restored * Add repeat validation * Misc. clean-up and tweaking * Fix bugprone-integer-division * More clean-up * Set entity_category for standby_switch * Set default entity_category for numbers * More housekeeping * Add run request tracking * Fix time remaining calculation * Use native unit_of_measurement for run duration numbers * Unstack some ifs --- esphome/components/sprinkler/__init__.py | 264 ++++++++- esphome/components/sprinkler/automation.h | 18 +- esphome/components/sprinkler/sprinkler.cpp | 633 ++++++++++++++++----- esphome/components/sprinkler/sprinkler.h | 115 +++- 4 files changed, 854 insertions(+), 176 deletions(-) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 52de290c85..cf3f471234 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -2,23 +2,36 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +from esphome.components import number from esphome.components import switch from esphome.const import ( + CONF_ENTITY_CATEGORY, CONF_ID, + CONF_INITIAL_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, CONF_NAME, CONF_REPEAT, + CONF_RESTORE_VALUE, CONF_RUN_DURATION, + CONF_STEP, + CONF_UNIT_OF_MEASUREMENT, ENTITY_CATEGORY_CONFIG, + UNIT_MINUTE, + UNIT_SECOND, ) -AUTO_LOAD = ["switch"] +AUTO_LOAD = ["number", "switch"] CODEOWNERS = ["@kbx81"] CONF_AUTO_ADVANCE_SWITCH = "auto_advance_switch" +CONF_DIVIDER = "divider" CONF_ENABLE_SWITCH = "enable_switch" CONF_MAIN_SWITCH = "main_switch" CONF_MANUAL_SELECTION_DELAY = "manual_selection_delay" CONF_MULTIPLIER = "multiplier" +CONF_MULTIPLIER_NUMBER = "multiplier_number" +CONF_NEXT_PREV_IGNORE_DISABLED = "next_prev_ignore_disabled" CONF_PUMP_OFF_SWITCH_ID = "pump_off_switch_id" CONF_PUMP_ON_SWITCH_ID = "pump_on_switch_id" CONF_PUMP_PULSE_DURATION = "pump_pulse_duration" @@ -30,7 +43,11 @@ CONF_PUMP_SWITCH = "pump_switch" CONF_PUMP_SWITCH_ID = "pump_switch_id" CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY = "pump_switch_off_during_valve_open_delay" CONF_QUEUE_ENABLE_SWITCH = "queue_enable_switch" +CONF_REPEAT_NUMBER = "repeat_number" CONF_REVERSE_SWITCH = "reverse_switch" +CONF_RUN_DURATION_NUMBER = "run_duration_number" +CONF_SET_ACTION = "set_action" +CONF_STANDBY_SWITCH = "standby_switch" CONF_VALVE_NUMBER = "valve_number" CONF_VALVE_OPEN_DELAY = "valve_open_delay" CONF_VALVE_OVERLAP = "valve_overlap" @@ -43,10 +60,14 @@ CONF_VALVES = "valves" sprinkler_ns = cg.esphome_ns.namespace("sprinkler") Sprinkler = sprinkler_ns.class_("Sprinkler", cg.Component) +SprinklerControllerNumber = sprinkler_ns.class_( + "SprinklerControllerNumber", number.Number, cg.Component +) SprinklerControllerSwitch = sprinkler_ns.class_( "SprinklerControllerSwitch", switch.Switch, cg.Component ) +SetDividerAction = sprinkler_ns.class_("SetDividerAction", automation.Action) SetMultiplierAction = sprinkler_ns.class_("SetMultiplierAction", automation.Action) QueueValveAction = sprinkler_ns.class_("QueueValveAction", automation.Action) ClearQueuedValvesAction = sprinkler_ns.class_( @@ -67,6 +88,19 @@ ResumeAction = sprinkler_ns.class_("ResumeAction", automation.Action) ResumeOrStartAction = sprinkler_ns.class_("ResumeOrStartAction", automation.Action) +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid(f"{CONF_MAX_VALUE} must be greater than {CONF_MIN_VALUE}") + + if (config[CONF_INITIAL_VALUE] > config[CONF_MAX_VALUE]) or ( + config[CONF_INITIAL_VALUE] < config[CONF_MIN_VALUE] + ): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) + return config + + def validate_sprinkler(config): for sprinkler_controller_index, sprinkler_controller in enumerate(config): if len(sprinkler_controller[CONF_VALVES]) <= 1: @@ -104,9 +138,18 @@ def validate_sprinkler(config): f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" ) + if ( + CONF_REPEAT in sprinkler_controller + and CONF_REPEAT_NUMBER in sprinkler_controller + ): + raise cv.Invalid( + f"Do not specify {CONF_REPEAT} when using {CONF_REPEAT_NUMBER}; use number component's {CONF_INITIAL_VALUE} instead" + ) + for valve in sprinkler_controller[CONF_VALVES]: if ( CONF_VALVE_OVERLAP in sprinkler_controller + and CONF_RUN_DURATION in valve and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OVERLAP] ): raise cv.Invalid( @@ -114,6 +157,7 @@ def validate_sprinkler(config): ) if ( CONF_VALVE_OPEN_DELAY in sprinkler_controller + and CONF_RUN_DURATION in valve and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OPEN_DELAY] ): @@ -170,6 +214,14 @@ def validate_sprinkler(config): raise cv.Invalid( f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration" ) + if CONF_RUN_DURATION not in valve and CONF_RUN_DURATION_NUMBER not in valve: + raise cv.Invalid( + f"Either {CONF_RUN_DURATION} or {CONF_RUN_DURATION_NUMBER} must be specified for each valve" + ) + if CONF_RUN_DURATION in valve and CONF_RUN_DURATION_NUMBER in valve: + raise cv.Invalid( + f"Do not specify {CONF_RUN_DURATION} when using {CONF_RUN_DURATION_NUMBER}; use number component's {CONF_INITIAL_VALUE} instead" + ) return config @@ -190,11 +242,20 @@ SPRINKLER_ACTION_REPEAT_SCHEMA = cv.maybe_simple_value( SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(Sprinkler), + cv.Optional(CONF_RUN_DURATION): cv.templatable(cv.positive_time_period_seconds), cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), }, key=CONF_VALVE_NUMBER, ) +SPRINKLER_ACTION_SET_DIVIDER_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_DIVIDER): cv.templatable(cv.positive_int), + }, + key=CONF_DIVIDER, +) + SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(Sprinkler), @@ -232,7 +293,30 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), - cv.Required(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=900): cv.positive_int, + cv.Optional(CONF_MAX_VALUE, default=86400): cv.positive_int, + cv.Optional(CONF_MIN_VALUE, default=1): cv.positive_int, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=1): cv.positive_int, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND + ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Required(CONF_VALVE_SWITCH): cv.maybe_simple_value( switch.switch_schema(SprinklerControllerSwitch), key=CONF_NAME, @@ -268,8 +352,55 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( ), key=CONF_NAME, ), + cv.Optional(CONF_STANDBY_SWITCH): cv.maybe_simple_value( + switch.switch_schema( + SprinklerControllerSwitch, entity_category=ENTITY_CATEGORY_CONFIG + ), + key=CONF_NAME, + ), + cv.Optional(CONF_NEXT_PREV_IGNORE_DISABLED, default=False): cv.boolean, cv.Optional(CONF_MANUAL_SELECTION_DELAY): cv.positive_time_period_seconds, + cv.Optional(CONF_MULTIPLIER_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=1): cv.positive_float, + cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_float, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=0.1): cv.positive_float, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Optional(CONF_REPEAT): cv.positive_int, + cv.Optional(CONF_REPEAT_NUMBER): cv.maybe_simple_value( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerNumber), + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + cv.Optional(CONF_INITIAL_VALUE, default=0): cv.positive_int, + cv.Optional(CONF_MAX_VALUE, default=10): cv.positive_int, + cv.Optional(CONF_MIN_VALUE, default=0): cv.positive_int, + cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean, + cv.Optional(CONF_STEP, default=1): cv.positive_int, + cv.Optional(CONF_SET_ACTION): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_min_max, + key=CONF_NAME, + ), cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, cv.Exclusive( @@ -301,6 +432,19 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "sprinkler.set_divider", + SetDividerAction, + SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, +) +async def sprinkler_set_divider_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_DIVIDER], args, cg.float_) + cg.add(var.set_divider(template_)) + return var + + @automation.register_action( "sprinkler.set_multiplier", SetMultiplierAction, @@ -385,6 +529,9 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) cg.add(var.set_valve_to_start(template_)) + if CONF_RUN_DURATION in config: + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) return var @@ -455,6 +602,79 @@ async def to_code(config): ) cg.add(var.set_controller_reverse_switch(sw_rev_var)) + if CONF_STANDBY_SWITCH in sprinkler_controller: + sw_stb_var = await switch.new_switch( + sprinkler_controller[CONF_STANDBY_SWITCH] + ) + await cg.register_component( + sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH] + ) + cg.add(var.set_controller_standby_switch(sw_stb_var)) + + if CONF_MULTIPLIER_NUMBER in sprinkler_controller: + num_mult_var = await number.new_number( + sprinkler_controller[CONF_MULTIPLIER_NUMBER], + min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ + CONF_MIN_VALUE + ], + max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ + CONF_MAX_VALUE + ], + step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP], + ) + await cg.register_component( + num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER] + ) + cg.add( + num_mult_var.set_initial_value( + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_mult_var.set_restore_value( + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]: + await automation.build_automation( + num_mult_var.get_set_trigger(), + [(float, "x")], + sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.set_controller_multiplier_number(num_mult_var)) + + if CONF_REPEAT_NUMBER in sprinkler_controller: + num_repeat_var = await number.new_number( + sprinkler_controller[CONF_REPEAT_NUMBER], + min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE], + max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE], + step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP], + ) + await cg.register_component( + num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER] + ) + cg.add( + num_repeat_var.set_initial_value( + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_repeat_var.set_restore_value( + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]: + await automation.build_automation( + num_repeat_var.get_set_trigger(), + [(float, "x")], + sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.set_controller_repeat_number(num_repeat_var)) + for valve in sprinkler_controller[CONF_VALVES]: sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH]) await cg.register_component(sw_valve_var, valve[CONF_VALVE_SWITCH]) @@ -470,6 +690,12 @@ async def to_code(config): else: cg.add(var.add_valve(sw_valve_var)) + cg.add( + var.set_next_prev_ignore_disabled_valves( + sprinkler_controller[CONF_NEXT_PREV_IGNORE_DISABLED] + ) + ) + if CONF_MANUAL_SELECTION_DELAY in sprinkler_controller: cg.add( var.set_manual_selection_delay( @@ -524,6 +750,11 @@ async def to_code(config): for sprinkler_controller in config: var = await cg.get_variable(sprinkler_controller[CONF_ID]) for valve_index, valve in enumerate(sprinkler_controller[CONF_VALVES]): + if CONF_RUN_DURATION not in valve: + valve[CONF_RUN_DURATION] = valve[CONF_RUN_DURATION_NUMBER][ + CONF_INITIAL_VALUE + ] + if CONF_VALVE_SWITCH_ID in valve: valve_switch = await cg.get_variable(valve[CONF_VALVE_SWITCH_ID]) cg.add( @@ -561,6 +792,35 @@ async def to_code(config): ) ) + if CONF_RUN_DURATION_NUMBER in valve: + num_rd_var = await number.new_number( + valve[CONF_RUN_DURATION_NUMBER], + min_value=valve[CONF_RUN_DURATION_NUMBER][CONF_MIN_VALUE], + max_value=valve[CONF_RUN_DURATION_NUMBER][CONF_MAX_VALUE], + step=valve[CONF_RUN_DURATION_NUMBER][CONF_STEP], + ) + await cg.register_component(num_rd_var, valve[CONF_RUN_DURATION_NUMBER]) + + cg.add( + num_rd_var.set_initial_value( + valve[CONF_RUN_DURATION_NUMBER][CONF_INITIAL_VALUE] + ) + ) + cg.add( + num_rd_var.set_restore_value( + valve[CONF_RUN_DURATION_NUMBER][CONF_RESTORE_VALUE] + ) + ) + + if CONF_SET_ACTION in valve[CONF_RUN_DURATION_NUMBER]: + await automation.build_automation( + num_rd_var.get_set_trigger(), + [(float, "x")], + valve[CONF_RUN_DURATION_NUMBER][CONF_SET_ACTION], + ) + + cg.add(var.configure_valve_run_duration_number(valve_index, num_rd_var)) + for sprinkler_controller in config: var = await cg.get_variable(sprinkler_controller[CONF_ID]) for controller_to_add in config: diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index dd0ea44633..59c6cd50e1 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -7,6 +7,18 @@ namespace esphome { namespace sprinkler { +template class SetDividerAction : public Action { + public: + explicit SetDividerAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(uint32_t, divider) + + void play(Ts... x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + template class SetMultiplierAction : public Action { public: explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} @@ -98,8 +110,12 @@ template class StartSingleValveAction : public Action { explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} TEMPLATABLE_VALUE(size_t, valve_to_start) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...)); } + void play(Ts... x) override { + this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } protected: Sprinkler *sprinkler_; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 2be71a08d0..9d3044802d 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -75,6 +75,34 @@ void SprinklerSwitch::sync_valve_state(bool latch_state) { } } +void SprinklerControllerNumber::setup() { + float value; + if (!this->restore_value_) { + value = this->initial_value_; + } else { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + if (!std::isnan(this->initial_value_)) { + value = this->initial_value_; + } else { + value = this->traits.get_min_value(); + } + } + } + this->publish_state(value); +} + +void SprinklerControllerNumber::control(float value) { + this->set_trigger_->trigger(value); + + this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); +} + +void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); } + SprinklerControllerSwitch::SprinklerControllerSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} @@ -101,12 +129,9 @@ void SprinklerControllerSwitch::write_state(bool state) { this->turn_off_trigger_->trigger(); } - if (this->optimistic_) - this->publish_state(state); + this->publish_state(state); } -void SprinklerControllerSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -bool SprinklerControllerSwitch::assumed_state() { return this->assumed_state_; } void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -114,30 +139,16 @@ Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this- Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void SprinklerControllerSwitch::setup() { - if (!this->restore_state_) - return; + this->state = this->get_initial_state_with_restore_mode().value_or(false); - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; - - ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); - if (*restored) { + if (this->state) { this->turn_on(); } else { this->turn_off(); } } -void SprinklerControllerSwitch::dump_config() { - LOG_SWITCH("", "Sprinkler Switch", this); - ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); - ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); -} - -void SprinklerControllerSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } - -void SprinklerControllerSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } +void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); } SprinklerValveOperator::SprinklerValveOperator() {} SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller) @@ -328,6 +339,8 @@ SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } +void SprinklerValveRunRequest::set_request_from(SprinklerValveRunRequestOrigin origin) { this->origin_ = origin; } + void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } void SprinklerValveRunRequest::set_valve(size_t valve_number) { @@ -345,6 +358,7 @@ void SprinklerValveRunRequest::set_valve_operator(SprinklerValveOperator *valve_ void SprinklerValveRunRequest::reset() { this->has_valve_ = false; + this->origin_ = USER; this->run_duration_ = 0; this->valve_op_ = nullptr; } @@ -362,6 +376,8 @@ optional SprinklerValveRunRequest::valve_as_opt() { SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } +SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } + Sprinkler::Sprinkler() {} Sprinkler::Sprinkler(const std::string &name) : EntityBase(name) {} @@ -404,8 +420,6 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll if (enable_sw != nullptr) { new_valve->enable_switch = enable_sw; - new_valve->enable_switch->set_optimistic(true); - new_valve->enable_switch->set_restore_state(true); } } @@ -433,26 +447,37 @@ void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller void Sprinkler::set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { this->auto_adv_sw_ = auto_adv_switch; - auto_adv_switch->set_optimistic(true); - auto_adv_switch->set_restore_state(true); } void Sprinkler::set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { this->queue_enable_sw_ = queue_enable_switch; - queue_enable_switch->set_optimistic(true); - queue_enable_switch->set_restore_state(true); } void Sprinkler::set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { this->reverse_sw_ = reverse_switch; - reverse_switch->set_optimistic(true); - reverse_switch->set_restore_state(true); +} + +void Sprinkler::set_controller_standby_switch(SprinklerControllerSwitch *standby_switch) { + this->standby_sw_ = standby_switch; + + this->sprinkler_standby_turn_on_automation_ = make_unique>(standby_switch->get_turn_on_trigger()); + this->sprinkler_standby_shutdown_action_ = make_unique>(this); + this->sprinkler_standby_turn_on_automation_->add_actions({sprinkler_standby_shutdown_action_.get()}); +} + +void Sprinkler::set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number) { + this->multiplier_number_ = multiplier_number; +} + +void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_number) { + this->repeat_number_ = repeat_number; } void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); this->valve_[valve_number].run_duration = run_duration; + valve_switch->turn_off(); } } @@ -464,6 +489,8 @@ void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Swit this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); this->valve_[valve_number].run_duration = run_duration; + valve_switch_off->turn_off(); + valve_switch_on->turn_off(); } } @@ -478,6 +505,7 @@ void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch this->pump_.resize(this->pump_.size() + 1); this->pump_.back().set_on_switch(pump_switch); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump + pump_switch->turn_off(); } } @@ -496,15 +524,49 @@ void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_: this->pump_.back().set_on_switch(pump_switch_on); this->pump_.back().set_pulse_duration(pulse_duration); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump + pump_switch_off->turn_off(); + pump_switch_on->turn_off(); + } +} + +void Sprinkler::configure_valve_run_duration_number(size_t valve_number, + SprinklerControllerNumber *run_duration_number) { + if (this->is_a_valid_valve(valve_number)) { + this->valve_[valve_number].run_duration_number = run_duration_number; + } +} + +void Sprinkler::set_divider(optional divider) { + if (!divider.has_value()) { + return; + } + if (divider.value() > 0) { + this->set_multiplier(1.0 / divider.value()); + this->set_repeat(divider.value() - 1); + } else if (divider.value() == 0) { + this->set_multiplier(1.0); + this->set_repeat(0); } } void Sprinkler::set_multiplier(const optional multiplier) { - if (multiplier.has_value()) { - if (multiplier.value() > 0) { - this->multiplier_ = multiplier.value(); - } + if ((!multiplier.has_value()) || (multiplier.value() < 0)) { + return; } + this->multiplier_ = multiplier.value(); + if (this->multiplier_number_ == nullptr) { + return; + } + if (this->multiplier_number_->state == multiplier.value()) { + return; + } + auto call = this->multiplier_number_->make_call(); + call.set_value(multiplier.value()); + call.perform(); +} + +void Sprinkler::set_next_prev_ignore_disabled_valves(bool ignore_disabled) { + this->next_prev_ignore_disabled_ = ignore_disabled; } void Sprinkler::set_pump_start_delay(uint32_t start_delay) { @@ -559,47 +621,118 @@ void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) { } void Sprinkler::set_valve_run_duration(const optional valve_number, const optional run_duration) { - if (valve_number.has_value() && run_duration.has_value()) { - if (this->is_a_valid_valve(valve_number.value())) { - this->valve_[valve_number.value()].run_duration = run_duration.value(); - } + if (!valve_number.has_value() || !run_duration.has_value()) { + return; } + if (!this->is_a_valid_valve(valve_number.value())) { + return; + } + this->valve_[valve_number.value()].run_duration = run_duration.value(); + if (this->valve_[valve_number.value()].run_duration_number == nullptr) { + return; + } + if (this->valve_[valve_number.value()].run_duration_number->state == run_duration.value()) { + return; + } + auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == min_str) { + call.set_value(run_duration.value() / 60.0); + } else { + call.set_value(run_duration.value()); + } + call.perform(); } void Sprinkler::set_auto_advance(const bool auto_advance) { - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(auto_advance); + if (this->auto_adv_sw_ == nullptr) { + return; + } + if (this->auto_adv_sw_->state == auto_advance) { + return; + } + if (auto_advance) { + this->auto_adv_sw_->turn_on(); + } else { + this->auto_adv_sw_->turn_off(); } } -void Sprinkler::set_repeat(optional repeat) { this->target_repeats_ = repeat; } +void Sprinkler::set_repeat(optional repeat) { + this->target_repeats_ = repeat; + if (this->repeat_number_ == nullptr) { + return; + } + if (this->repeat_number_->state == repeat.value()) { + return; + } + auto call = this->repeat_number_->make_call(); + call.set_value(repeat.value_or(0)); + call.perform(); +} void Sprinkler::set_queue_enable(bool queue_enable) { - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(queue_enable); + if (this->queue_enable_sw_ == nullptr) { + return; + } + if (this->queue_enable_sw_->state == queue_enable) { + return; + } + if (queue_enable) { + this->queue_enable_sw_->turn_on(); + } else { + this->queue_enable_sw_->turn_off(); } } void Sprinkler::set_reverse(const bool reverse) { - if (this->reverse_sw_ != nullptr) { - this->reverse_sw_->publish_state(reverse); + if (this->reverse_sw_ == nullptr) { + return; + } + if (this->reverse_sw_->state == reverse) { + return; + } + if (reverse) { + this->reverse_sw_->turn_on(); + } else { + this->reverse_sw_->turn_off(); + } +} + +void Sprinkler::set_standby(const bool standby) { + if (this->standby_sw_ == nullptr) { + return; + } + if (this->standby_sw_->state == standby) { + return; + } + if (standby) { + this->standby_sw_->turn_on(); + } else { + this->standby_sw_->turn_off(); } } uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { - if (this->is_a_valid_valve(valve_number)) { - return this->valve_[valve_number].run_duration; + if (!this->is_a_valid_valve(valve_number)) { + return 0; } - return 0; + if (this->valve_[valve_number].run_duration_number != nullptr) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == min_str) { + return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); + } else { + return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); + } + } + return this->valve_[valve_number].run_duration; } uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) { uint32_t run_duration = 0; if (this->is_a_valid_valve(valve_number)) { - run_duration = this->valve_[valve_number].run_duration; + run_duration = this->valve_run_duration(valve_number); } - run_duration = static_cast(roundf(run_duration * this->multiplier_)); + run_duration = static_cast(roundf(run_duration * this->multiplier())); // run_duration must not be less than any of these if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) || (run_duration < this->switching_delay_.value_or(0) * 2)) { @@ -615,16 +748,24 @@ bool Sprinkler::auto_advance() { return false; } -float Sprinkler::multiplier() { return this->multiplier_; } +float Sprinkler::multiplier() { + if (this->multiplier_number_ != nullptr) { + return this->multiplier_number_->state; + } + return this->multiplier_; +} -optional Sprinkler::repeat() { return this->target_repeats_; } +optional Sprinkler::repeat() { + if (this->repeat_number_ != nullptr) { + return static_cast(roundf(this->repeat_number_->state)); + } + return this->target_repeats_; +} optional Sprinkler::repeat_count() { // if there is an active valve and auto-advance is enabled, we may be repeating, so return the count - if (this->auto_adv_sw_ != nullptr) { - if (this->active_req_.has_request() && this->auto_adv_sw_->state) { - return this->repeat_count_; - } + if (this->active_req_.has_request() && this->auto_advance()) { + return this->repeat_count_; } return nullopt; } @@ -643,7 +784,22 @@ bool Sprinkler::reverse() { return false; } +bool Sprinkler::standby() { + if (this->standby_sw_ != nullptr) { + return this->standby_sw_->state; + } + return false; +} + void Sprinkler::start_from_queue() { + if (this->standby()) { + ESP_LOGD(TAG, "start_from_queue called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_from_queue called but multiplier is set to zero; no action taken"); + return; + } if (this->queued_valves_.empty()) { return; // if there is nothing in the queue, don't do anything } @@ -651,25 +807,29 @@ void Sprinkler::start_from_queue() { return; // if there is already a valve running from the queue, do nothing } - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(false); - } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(true); - } + this->set_auto_advance(false); + this->set_queue_enable(true); + this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; this->fsm_kick_(); // will automagically pick up from the queue (it has priority) } void Sprinkler::start_full_cycle() { + if (this->standby()) { + ESP_LOGD(TAG, "start_full_cycle called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_full_cycle called but multiplier is set to zero; no action taken"); + return; + } if (this->auto_advance() && this->active_valve().has_value()) { return; // if auto-advance is already enabled and there is already a valve running, do nothing } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(false); - } + this->set_queue_enable(false); + this->prep_full_cycle_(); this->repeat_count_ = 0; // if there is no active valve already, start the first valve in the cycle @@ -678,20 +838,25 @@ void Sprinkler::start_full_cycle() { } } -void Sprinkler::start_single_valve(const optional valve_number) { +void Sprinkler::start_single_valve(const optional valve_number, optional run_duration) { + if (this->standby()) { + ESP_LOGD(TAG, "start_single_valve called but standby is enabled; no action taken"); + return; + } + if (this->multiplier() == 0) { + ESP_LOGD(TAG, "start_single_valve called but multiplier is set to zero; no action taken"); + return; + } if (!valve_number.has_value() || (valve_number == this->active_valve())) { return; } - if (this->auto_adv_sw_ != nullptr) { - this->auto_adv_sw_->publish_state(false); - } - if (this->queue_enable_sw_ != nullptr) { - this->queue_enable_sw_->publish_state(false); - } + this->set_auto_advance(false); + this->set_queue_enable(false); + this->reset_cycle_states_(); // just in case auto-advance is switched on later this->repeat_count_ = 0; - this->fsm_request_(valve_number.value()); + this->fsm_request_(valve_number.value(), run_duration.value_or(0)); } void Sprinkler::queue_valve(optional valve_number, optional run_duration) { @@ -714,8 +879,17 @@ void Sprinkler::next_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } + this->manual_valve_ = this->next_valve_number_( - this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1))); + this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1)), + !this->next_prev_ignore_disabled_, true); + + if (!this->manual_valve_.has_value()) { + ESP_LOGD(TAG, "next_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows only " + "enabled valves and no valves are enabled?"); + return; + } + if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); @@ -728,8 +902,17 @@ void Sprinkler::previous_valve() { if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } + this->manual_valve_ = - this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0))); + this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0)), + !this->next_prev_ignore_disabled_, true); + + if (!this->manual_valve_.has_value()) { + ESP_LOGD(TAG, "previous_valve was called but no valve could be started; perhaps next_prev_ignore_disabled allows " + "only enabled valves and no valves are enabled?"); + return; + } + if (this->manual_selection_delay_.has_value()) { this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); @@ -758,7 +941,7 @@ void Sprinkler::pause() { return; // we can't pause if we're already paused or if there is no active valve } this->paused_valve_ = this->active_valve(); - this->resume_duration_ = this->time_remaining(); + this->resume_duration_ = this->time_remaining_active_valve(); this->shutdown(false); ESP_LOGD(TAG, "Paused valve %u with %u seconds remaining", this->paused_valve_.value_or(0), this->resume_duration_.value_or(0)); @@ -795,6 +978,13 @@ const char *Sprinkler::valve_name(const size_t valve_number) { return nullptr; } +optional Sprinkler::active_valve_request_is_from() { + if (this->active_req_.has_request()) { + return this->active_req_.request_is_from(); + } + return nullopt; +} + optional Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } optional Sprinkler::paused_valve() { return this->paused_valve_; } @@ -829,8 +1019,7 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or - // is - // STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now + // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now if ((vo.state() == ACTIVE) || ((vo.state() == STARTING) && this->start_delay_ && this->start_delay_is_valve_delay_) || ((vo.state() == STOPPING) && this->stop_delay_ && this->stop_delay_is_valve_delay_)) { @@ -881,7 +1070,93 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { } } -optional Sprinkler::time_remaining() { +uint32_t Sprinkler::total_cycle_time_all_valves() { + uint32_t total_time_remaining = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + } + + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (this->number_of_valves() - 1); + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_cycle_time_enabled_valves() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + if (this->valve_is_enabled_(valve)) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + valve_count++; + } + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (size_t valve = 0; valve < this->number_of_valves(); valve++) { + if (this->valve_is_enabled_(valve) && !this->valve_cycle_complete_(valve)) { + if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { + total_time_remaining += this->valve_run_duration_adjusted(valve); + valve_count++; + } + } + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +uint32_t Sprinkler::total_queue_time() { + uint32_t total_time_remaining = 0; + uint32_t valve_count = 0; + + for (auto &valve : this->queued_valves_) { + if (valve.run_duration) { + total_time_remaining += valve.run_duration; + } else { + total_time_remaining += this->valve_run_duration_adjusted(valve.valve_number); + } + valve_count++; + } + + if (valve_count) { + if (this->valve_overlap_) { + total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); + } else { + total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); + } + } + + return total_time_remaining; +} + +optional Sprinkler::time_remaining_active_valve() { if (this->active_req_.has_request()) { // first try to return the value based on active_req_... if (this->active_req_.valve_operator() != nullptr) { return this->active_req_.valve_operator()->time_remaining(); @@ -895,6 +1170,25 @@ optional Sprinkler::time_remaining() { return nullopt; } +optional Sprinkler::time_remaining_current_operation() { + auto total_time_remaining = this->time_remaining_active_valve(); + + if (total_time_remaining.has_value()) { + if (this->auto_advance()) { + total_time_remaining = total_time_remaining.value() + this->total_cycle_time_enabled_incomplete_valves(); + total_time_remaining = + total_time_remaining.value() + + (this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0))); + } + + if (this->queue_enabled()) { + total_time_remaining = total_time_remaining.value() + this->total_queue_time(); + } + return total_time_remaining; + } + return nullopt; +} + SprinklerControllerSwitch *Sprinkler::control_switch(size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { return this->valve_[valve_number].controller_switch; @@ -957,30 +1251,60 @@ bool Sprinkler::valve_cycle_complete_(const size_t valve_number) { return false; } -size_t Sprinkler::next_valve_number_(const size_t first_valve) { - if (this->is_a_valid_valve(first_valve) && (first_valve + 1 < this->number_of_valves())) - return first_valve + 1; +optional Sprinkler::next_valve_number_(const optional first_valve, const bool include_disabled, + const bool include_complete) { + auto valve = first_valve.value_or(0); + size_t start = first_valve.has_value() ? 1 : 0; - return 0; + if (!this->is_a_valid_valve(valve)) { + valve = 0; + } + + for (size_t offset = start; offset < this->number_of_valves(); offset++) { + auto valve_of_interest = valve + offset; + if (!this->is_a_valid_valve(valve_of_interest)) { + valve_of_interest -= this->number_of_valves(); + } + + if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && + (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { + return valve_of_interest; + } + } + return nullopt; } -size_t Sprinkler::previous_valve_number_(const size_t first_valve) { - if (this->is_a_valid_valve(first_valve) && (first_valve - 1 >= 0)) - return first_valve - 1; +optional Sprinkler::previous_valve_number_(const optional first_valve, const bool include_disabled, + const bool include_complete) { + auto valve = first_valve.value_or(this->number_of_valves() - 1); + size_t start = first_valve.has_value() ? 1 : 0; - return this->number_of_valves() - 1; + if (!this->is_a_valid_valve(valve)) { + valve = this->number_of_valves() - 1; + } + + for (size_t offset = start; offset < this->number_of_valves(); offset++) { + auto valve_of_interest = valve - offset; + if (!this->is_a_valid_valve(valve_of_interest)) { + valve_of_interest += this->number_of_valves(); + } + + if ((this->valve_is_enabled_(valve_of_interest) || include_disabled) && + (!this->valve_cycle_complete_(valve_of_interest) || include_complete)) { + return valve_of_interest; + } + } + return nullopt; } optional Sprinkler::next_valve_number_in_cycle_(const optional first_valve) { - if (this->reverse_sw_ != nullptr) { - if (this->reverse_sw_->state) { - return this->previous_enabled_incomplete_valve_number_(first_valve); - } + if (this->reverse()) { + return this->previous_valve_number_(first_valve, false, false); } - return this->next_enabled_incomplete_valve_number_(first_valve); + return this->next_valve_number_(first_valve, false, false); } -void Sprinkler::load_next_valve_run_request_(optional first_valve) { +void Sprinkler::load_next_valve_run_request_(const optional first_valve) { if (this->next_req_.has_request()) { if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve())); @@ -988,58 +1312,37 @@ void Sprinkler::load_next_valve_run_request_(optional first_valve) { return; // there is already a request pending } else if (this->queue_enabled() && !this->queued_valves_.empty()) { this->next_req_.set_valve(this->queued_valves_.back().valve_number); + this->next_req_.set_request_from(QUEUE); if (this->queued_valves_.back().run_duration) { this->next_req_.set_run_duration(this->queued_valves_.back().run_duration); - } else { + this->queued_valves_.pop_back(); + } else if (this->multiplier()) { this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number)); + this->queued_valves_.pop_back(); + } else { + this->next_req_.reset(); } - this->queued_valves_.pop_back(); - } else if (this->auto_adv_sw_ != nullptr) { - if (this->auto_adv_sw_->state) { - if (this->next_valve_number_in_cycle_(first_valve).has_value()) { - // if there is another valve to run as a part of a cycle, load that - this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + } else if (this->auto_advance() && this->multiplier()) { + if (this->next_valve_number_in_cycle_(first_valve).has_value()) { + // if there is another valve to run as a part of a cycle, load that + this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + this->next_req_.set_request_from(CYCLE); + this->next_req_.set_run_duration( + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + } else if ((this->repeat_count_++ < this->repeat().value_or(0))) { + ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, this->repeat().value_or(0) + 1); + // if there are repeats remaining and no more valves were left in the cycle, start a new cycle + this->prep_full_cycle_(); + if (this->next_valve_number_in_cycle_().has_value()) { // this should always succeed here, but just in case... + this->next_req_.set_valve(this->next_valve_number_in_cycle_().value_or(0)); + this->next_req_.set_request_from(CYCLE); this->next_req_.set_run_duration( - this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); - } else if ((this->repeat_count_++ < this->target_repeats_.value_or(0))) { - ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, - this->target_repeats_.value_or(0) + 1); - // if there are repeats remaining and no more valves were left in the cycle, start a new cycle - this->prep_full_cycle_(); - this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); - this->next_req_.set_run_duration( - this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_().value_or(0))); } } } } -optional Sprinkler::next_enabled_incomplete_valve_number_(const optional first_valve) { - auto new_valve_number = this->next_valve_number_(first_valve.value_or(this->number_of_valves() - 1)); - - while (new_valve_number != first_valve.value_or(this->number_of_valves() - 1)) { - if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { - return new_valve_number; - } else { - new_valve_number = this->next_valve_number_(new_valve_number); - } - } - return nullopt; -} - -optional Sprinkler::previous_enabled_incomplete_valve_number_(const optional first_valve) { - auto new_valve_number = this->previous_valve_number_(first_valve.value_or(0)); - - while (new_valve_number != first_valve.value_or(0)) { - if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { - return new_valve_number; - } else { - new_valve_number = this->previous_valve_number_(new_valve_number); - } - } - return nullopt; -} - bool Sprinkler::any_valve_is_enabled_() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { if (this->valve_is_enabled_(valve_number)) @@ -1058,8 +1361,9 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up if (vo.state() == IDLE) { auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); - ESP_LOGD(TAG, "Starting valve %u for %u seconds, cycle %u of %u", req->valve(), run_duration, - this->repeat_count_ + 1, this->target_repeats_.value_or(0) + 1); + ESP_LOGD(TAG, "%s is starting valve %u for %u seconds, cycle %u of %u", + this->req_as_str_(req->request_is_from()).c_str(), req->valve(), run_duration, this->repeat_count_ + 1, + this->repeat().value_or(0) + 1); req->set_valve_operator(&vo); vo.set_controller(this); vo.set_valve(&this->valve_[req->valve()]); @@ -1085,15 +1389,14 @@ void Sprinkler::all_valves_off_(const bool include_pump) { } void Sprinkler::prep_full_cycle_() { - if (this->auto_adv_sw_ != nullptr) { - if (!this->auto_adv_sw_->state) { - this->auto_adv_sw_->publish_state(true); - } - } + this->set_auto_advance(true); + if (!this->any_valve_is_enabled_()) { for (auto &valve : this->valve_) { if (valve.enable_switch != nullptr) { - valve.enable_switch->publish_state(true); + if (!valve.enable_switch->state) { + valve.enable_switch->turn_on(); + } } } } @@ -1169,14 +1472,19 @@ void Sprinkler::fsm_transition_() { void Sprinkler::fsm_transition_from_shutdown_() { this->load_next_valve_run_request_(); - this->active_req_.set_valve(this->next_req_.valve()); - this->active_req_.set_run_duration(this->next_req_.run_duration()); - this->next_req_.reset(); - this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); - this->start_timer_(sprinkler::TIMER_SM); - this->start_valve_(&this->active_req_); - this->state_ = ACTIVE; + if (this->next_req_.has_request()) { // there is a valve to run... + this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_request_from(this->next_req_.request_is_from()); + this->active_req_.set_run_duration(this->next_req_.run_duration()); + this->next_req_.reset(); + + this->set_timer_duration_(sprinkler::TIMER_SM, + this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + this->start_timer_(sprinkler::TIMER_SM); + this->start_valve_(&this->active_req_); + this->state_ = ACTIVE; + } } void Sprinkler::fsm_transition_from_valve_run_() { @@ -1186,7 +1494,9 @@ void Sprinkler::fsm_transition_from_valve_run_() { } if (!this->timer_active_(sprinkler::TIMER_SM)) { // only flag the valve as "complete" if the timer finished - this->mark_valve_cycle_complete_(this->active_req_.valve()); + if ((this->active_req_.request_is_from() == CYCLE) || (this->active_req_.request_is_from() == USER)) { + this->mark_valve_cycle_complete_(this->active_req_.valve()); + } } else { ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve"); for (auto &vo : this->valve_op_) { @@ -1201,6 +1511,7 @@ void Sprinkler::fsm_transition_from_valve_run_() { this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve()); this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_request_from(this->next_req_.request_is_from()); this->active_req_.set_run_duration(this->next_req_.run_duration()); this->next_req_.reset(); @@ -1230,6 +1541,22 @@ void Sprinkler::fsm_transition_to_shutdown_() { this->start_timer_(sprinkler::TIMER_SM); } +std::string Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { + switch (origin) { + case USER: + return "USER"; + + case CYCLE: + return "CYCLE"; + + case QUEUE: + return "QUEUE"; + + default: + return "UNKNOWN"; + } +} + std::string Sprinkler::state_as_str_(SprinklerState state) { switch (state) { case IDLE: @@ -1300,8 +1627,8 @@ void Sprinkler::dump_config() { if (this->manual_selection_delay_.has_value()) { ESP_LOGCONFIG(TAG, " Manual Selection Delay: %u seconds", this->manual_selection_delay_.value_or(0)); } - if (this->target_repeats_.has_value()) { - ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->target_repeats_.value_or(0)); + if (this->repeat().has_value()) { + ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->repeat().value_or(0)); } if (this->start_delay_) { if (this->start_delay_is_valve_delay_) { @@ -1329,7 +1656,7 @@ void Sprinkler::dump_config() { for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { ESP_LOGCONFIG(TAG, " Valve %u:", valve_number); ESP_LOGCONFIG(TAG, " Name: %s", this->valve_name(valve_number)); - ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_[valve_number].run_duration); + ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_run_duration(valve_number)); if (this->valve_[valve_number].valve_switch.pulse_duration()) { ESP_LOGCONFIG(TAG, " Pulse Duration: %u milliseconds", this->valve_[valve_number].valve_switch.pulse_duration()); diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 625118d9e5..1b8c7e4528 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -3,6 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/components/number/number.h" #include "esphome/components/switch/switch.h" #include @@ -10,6 +11,8 @@ namespace esphome { namespace sprinkler { +const std::string min_str = "min"; + enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! IDLE, // system/valve is off @@ -24,7 +27,14 @@ enum SprinklerTimerIndex : uint8_t { TIMER_VALVE_SELECTION = 1, }; +enum SprinklerValveRunRequestOrigin : uint8_t { + USER, + CYCLE, + QUEUE, +}; + class Sprinkler; // this component +class SprinklerControllerNumber; // number components that appear in the front end; based on number core class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps @@ -76,6 +86,7 @@ struct SprinklerTimer { }; struct SprinklerValve { + SprinklerControllerNumber *run_duration_number; SprinklerControllerSwitch *controller_switch; SprinklerControllerSwitch *enable_switch; SprinklerSwitch valve_switch; @@ -88,6 +99,25 @@ struct SprinklerValve { std::unique_ptr> valve_turn_on_automation; }; +class SprinklerControllerNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return set_trigger_; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + float initial_value_{NAN}; + bool restore_value_{true}; + Trigger *set_trigger_ = new Trigger(); + + ESPPreferenceObject pref_; +}; + class SprinklerControllerSwitch : public switch_::Switch, public Component { public: SprinklerControllerSwitch(); @@ -96,27 +126,19 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - void set_restore_state(bool restore_state); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; - void set_optimistic(bool optimistic); - void set_assumed_state(bool assumed_state); void loop() override; float get_setup_priority() const override; protected: - bool assumed_state() override; - void write_state(bool state) override; optional()>> f_; - bool optimistic_{false}; - bool assumed_state_{false}; Trigger<> *turn_on_trigger_; Trigger<> *turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; - bool restore_state_{false}; }; class SprinklerValveOperator { @@ -160,6 +182,7 @@ class SprinklerValveRunRequest { SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op); bool has_request(); bool has_valve_operator(); + void set_request_from(SprinklerValveRunRequestOrigin origin); void set_run_duration(uint32_t run_duration); void set_valve(size_t valve_number); void set_valve_operator(SprinklerValveOperator *valve_op); @@ -168,12 +191,14 @@ class SprinklerValveRunRequest { size_t valve(); optional valve_as_opt(); SprinklerValveOperator *valve_operator(); + SprinklerValveRunRequestOrigin request_is_from(); protected: bool has_valve_{false}; size_t valve_number_{0}; uint32_t run_duration_{0}; SprinklerValveOperator *valve_op_{nullptr}; + SprinklerValveRunRequestOrigin origin_{USER}; }; class Sprinkler : public Component, public EntityBase { @@ -196,6 +221,11 @@ class Sprinkler : public Component, public EntityBase { void set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch); void set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch); void set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch); + void set_controller_standby_switch(SprinklerControllerSwitch *standby_switch); + + /// configure important controller number components + void set_controller_multiplier_number(SprinklerControllerNumber *multiplier_number); + void set_controller_repeat_number(SprinklerControllerNumber *repeat_number); /// configure a valve's switch object and run duration. run_duration is time in seconds. void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); @@ -207,9 +237,18 @@ class Sprinkler : public Component, public EntityBase { void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, switch_::Switch *pump_switch_on, uint32_t pulse_duration); + /// configure a valve's run duration number component + void configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number); + + /// sets the multiplier value to '1 / divider' and sets repeat value to divider + void set_divider(optional divider); + /// value multiplied by configured run times -- used to extend or shorten the cycle void set_multiplier(optional multiplier); + /// enable/disable skipping of disabled valves by the next and previous actions + void set_next_prev_ignore_disabled_valves(bool ignore_disabled); + /// set how long the pump should start after the valve (when the pump is starting) void set_pump_start_delay(uint32_t start_delay); @@ -250,6 +289,9 @@ class Sprinkler : public Component, public EntityBase { /// if reverse is true, controller will iterate through all enabled valves in reverse (descending) order void set_reverse(bool reverse); + /// if standby is true, controller will refuse to activate any valves + void set_standby(bool standby); + /// returns valve_number's run duration in seconds uint32_t valve_run_duration(size_t valve_number); @@ -274,6 +316,9 @@ class Sprinkler : public Component, public EntityBase { /// returns true if reverse is enabled bool reverse(); + /// returns true if standby is enabled + bool standby(); + /// starts the controller from the first valve in the queue and disables auto_advance. /// if the queue is empty, does nothing. void start_from_queue(); @@ -283,7 +328,7 @@ class Sprinkler : public Component, public EntityBase { void start_full_cycle(); /// activates a single valve and disables auto_advance. - void start_single_valve(optional valve_number); + void start_single_valve(optional valve_number, optional run_duration = nullopt); /// adds a valve into the queue. queued valves have priority over valves to be run as a part of a full cycle. /// NOTE: queued valves will always run, regardless of auto-advance and/or valve enable switches. @@ -316,6 +361,9 @@ class Sprinkler : public Component, public EntityBase { /// returns a pointer to a valve's name string object; returns nullptr if valve_number is invalid const char *valve_name(size_t valve_number); + /// returns what invoked the valve that is currently active, if any. check with 'has_value()' + optional active_valve_request_is_from(); + /// returns the number of the valve that is currently active, if any. check with 'has_value()' optional active_valve(); @@ -341,8 +389,23 @@ class Sprinkler : public Component, public EntityBase { /// switches on/off a pump "safely" by checking that the new state will not conflict with another controller void set_pump_state(SprinklerSwitch *pump_switch, bool state); - /// returns the amount of time remaining in seconds for the active valve, if any. check with 'has_value()' - optional time_remaining(); + /// returns the amount of time in seconds required for all valves + uint32_t total_cycle_time_all_valves(); + + /// returns the amount of time in seconds required for all enabled valves + uint32_t total_cycle_time_enabled_valves(); + + /// returns the amount of time in seconds required for all enabled & incomplete valves, not including the active valve + uint32_t total_cycle_time_enabled_incomplete_valves(); + + /// returns the amount of time in seconds required for all valves in the queue + uint32_t total_queue_time(); + + /// returns the amount of time remaining in seconds for the active valve, if any + optional time_remaining_active_valve(); + + /// returns the amount of time remaining in seconds for all valves remaining, including the active valve, if any + optional time_remaining_current_operation(); /// returns a pointer to a valve's control switch object SprinklerControllerSwitch *control_switch(size_t valve_number); @@ -371,9 +434,13 @@ class Sprinkler : public Component, public EntityBase { /// returns true if valve's cycle is flagged as complete bool valve_cycle_complete_(size_t valve_number); - /// returns the number of the next/previous valve in the vector - size_t next_valve_number_(size_t first_valve); - size_t previous_valve_number_(size_t first_valve); + /// returns the number of the next valve in the vector or nullopt if no valves match criteria + optional next_valve_number_(optional first_valve = nullopt, bool include_disabled = true, + bool include_complete = true); + + /// returns the number of the previous valve in the vector or nullopt if no valves match criteria + optional previous_valve_number_(optional first_valve = nullopt, bool include_disabled = true, + bool include_complete = true); /// returns the number of the next valve that should be activated in a full cycle. /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') @@ -385,11 +452,6 @@ class Sprinkler : public Component, public EntityBase { /// if no valve is next (for example, a full cycle is complete), next_req_ is reset via reset(). void load_next_valve_run_request_(optional first_valve = nullopt); - /// returns the number of the next/previous valve that should be activated. - /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') - optional next_enabled_incomplete_valve_number_(optional first_valve); - optional previous_enabled_incomplete_valve_number_(optional first_valve); - /// returns true if any valve is enabled bool any_valve_is_enabled_(); @@ -424,7 +486,10 @@ class Sprinkler : public Component, public EntityBase { /// starts up the system from IDLE state void fsm_transition_to_shutdown_(); - /// return the current FSM state as a string + /// return the specified SprinklerValveRunRequestOrigin as a string + std::string req_as_str_(SprinklerValveRunRequestOrigin origin); + + /// return the specified SprinklerState state as a string std::string state_as_str_(SprinklerState state); /// Start/cancel/get status of valve timers @@ -446,6 +511,9 @@ class Sprinkler : public Component, public EntityBase { /// Maximum allowed queue size const uint8_t max_queue_size_{100}; + /// When set to true, the next and previous actions will skip disabled valves + bool next_prev_ignore_disabled_{false}; + /// Pump should be off during valve_open_delay interval bool pump_switch_off_during_valve_open_delay_{false}; @@ -518,12 +586,19 @@ class Sprinkler : public Component, public EntityBase { SprinklerControllerSwitch *controller_sw_{nullptr}; SprinklerControllerSwitch *queue_enable_sw_{nullptr}; SprinklerControllerSwitch *reverse_sw_{nullptr}; + SprinklerControllerSwitch *standby_sw_{nullptr}; + + /// Number components we'll present to the front end + SprinklerControllerNumber *multiplier_number_{nullptr}; + SprinklerControllerNumber *repeat_number_{nullptr}; std::unique_ptr> sprinkler_shutdown_action_; + std::unique_ptr> sprinkler_standby_shutdown_action_; std::unique_ptr> sprinkler_resumeorstart_action_; std::unique_ptr> sprinkler_turn_off_automation_; std::unique_ptr> sprinkler_turn_on_automation_; + std::unique_ptr> sprinkler_standby_turn_on_automation_; }; } // namespace sprinkler From 8e1430243e4955a711db2c40a2543f781a51408a Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 22 Feb 2023 15:40:20 -0800 Subject: [PATCH 052/115] fix parity (#4476) Co-authored-by: Samuel Sieb --- esphome/components/wiegand/wiegand.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index 67558da731..c4e834c85a 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -38,8 +38,8 @@ void Wiegand::setup() { bool check_eparity(uint64_t value, int start, int length) { int parity = 0; uint64_t mask = 1LL << start; - for (int i = 0; i <= length; i++, mask <<= 1) { - if (value & i) + for (int i = 0; i < length; i++, mask <<= 1) { + if (value & mask) parity++; } return !(parity & 1); @@ -48,8 +48,8 @@ bool check_eparity(uint64_t value, int start, int length) { bool check_oparity(uint64_t value, int start, int length) { int parity = 0; uint64_t mask = 1LL << start; - for (int i = 0; i <= length; i++, mask <<= 1) { - if (value & i) + for (int i = 0; i < length; i++, mask <<= 1) { + if (value & mask) parity++; } return parity & 1; From 91e037346bc23019d7a57d7d787cbb2e560dc1e5 Mon Sep 17 00:00:00 2001 From: Shreyas Karnik <311217+shreyaskarnik@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:37:23 -0800 Subject: [PATCH 053/115] add person sensor (SEN21231) from usefulsensors (#4454) * add person sensor (SEN21231) from usefulsensors * add person sensor (SEN21231) from usefulsensors * change file mode * fix tests * fix tests * rollback un-intended changes * Update esphome/components/sen21231/sen21231.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.h Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * remove unused import * Update esphome/components/sen21231/sen21231.h Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.h Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/sen21231/sen21231.h Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * remove unused import * Update sen21231.h * lint changes * linting * linting * Update sen21231.h * Update sen21231.cpp linting * linting fixes * fix codeowners --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sen21231/__init__.py | 0 esphome/components/sen21231/sen21231.cpp | 32 ++++++++++ esphome/components/sen21231/sen21231.h | 77 ++++++++++++++++++++++++ esphome/components/sen21231/sensor.py | 24 ++++++++ tests/test1.yaml | 4 +- 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 esphome/components/sen21231/__init__.py create mode 100644 esphome/components/sen21231/sen21231.cpp create mode 100644 esphome/components/sen21231/sen21231.h create mode 100644 esphome/components/sen21231/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 79c9c4f94b..d24a19adb1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -210,6 +210,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core diff --git a/esphome/components/sen21231/__init__.py b/esphome/components/sen21231/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp new file mode 100644 index 0000000000..aa123dff62 --- /dev/null +++ b/esphome/components/sen21231/sen21231.cpp @@ -0,0 +1,32 @@ +#include "sen21231.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sen21231_sensor { + +static const char *const TAG = "sen21231_sensor.sensor"; + +void Sen21231Sensor::update() { this->read_data_(); } + +void Sen21231Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "SEN21231:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with SEN21231 failed!"); + } + ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? "FAILED" : "OK"); + LOG_UPDATE_INTERVAL(this); +} + +void Sen21231Sensor::read_data_() { + person_sensor_results_t results; + this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results)); + ESP_LOGD(TAG, "SEN21231: %d faces detected", results.num_faces); + this->publish_state(results.num_faces); + if (results.num_faces == 1) { + ESP_LOGD(TAG, "SEN21231: is facing towards camera: %d", results.faces[0].is_facing); + } +} + +} // namespace sen21231_sensor +} // namespace esphome diff --git a/esphome/components/sen21231/sen21231.h b/esphome/components/sen21231/sen21231.h new file mode 100644 index 0000000000..b4d540df55 --- /dev/null +++ b/esphome/components/sen21231/sen21231.h @@ -0,0 +1,77 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +// ref: +// https://github.com/usefulsensors/person_sensor_pico_c/blob/main/person_sensor.h + +namespace esphome { +namespace sen21231_sensor { +// The I2C address of the person sensor board. +static const uint8_t PERSON_SENSOR_I2C_ADDRESS = 0x62; +static const uint8_t PERSON_SENSOR_REG_MODE = 0x01; +static const uint8_t PERSON_SENSOR_REG_ENABLE_ID = 0x02; +static const uint8_t PERSON_SENSOR_REG_SINGLE_SHOT = 0x03; +static const uint8_t PERSON_SENSOR_REG_CALIBRATE_ID = 0x04; +static const uint8_t PERSON_SENSOR_REG_PERSIST_IDS = 0x05; +static const uint8_t PERSON_SENSOR_REG_ERASE_IDS = 0x06; +static const uint8_t PERSON_SENSOR_REG_DEBUG_MODE = 0x07; + +static const uint8_t PERSON_SENSOR_MAX_FACES_COUNT = 4; +static const uint8_t PERSON_SENSOR_MAX_IDS_COUNT = 7; + +// The results returned from the sensor have a short header providing +// information about the length of the data packet: +// reserved: Currently unused bytes. +// data_size: Length of the entire packet, excluding the header and +// checksum. +// For version 1.0 of the sensor, this should be 40. +using person_sensor_results_header_t = struct { + uint8_t reserved[2]; // Bytes 0-1. + uint16_t data_size; // Bytes 2-3. +}; + +// Each face found has a set of information associated with it: +// box_confidence: How certain we are we have found a face, from 0 to 255. +// box_left: X coordinate of the left side of the box, from 0 to 255. +// box_top: Y coordinate of the top edge of the box, from 0 to 255. +// box_width: Width of the box, where 255 is the full view port size. +// box_height: Height of the box, where 255 is the full view port size. +// id_confidence: How sure the sensor is about the recognition result. +// id: Numerical ID assigned to this face. +// is_looking_at: Whether the person is facing the camera, 0 or 1. +using person_sensor_face_t = struct __attribute__((__packed__)) { + uint8_t box_confidence; // Byte 1. + uint8_t box_left; // Byte 2. + uint8_t box_top; // Byte 3. + uint8_t box_right; // Byte 4. + uint8_t box_bottom; // Byte 5. + int8_t id_confidence; // Byte 6. + int8_t id; // Byte 7 + uint8_t is_facing; // Byte 8. +}; + +// This is the full structure of the packet returned over the wire from the +// sensor when we do an I2C read from the peripheral address. +// The checksum should be the CRC16 of bytes 0 to 38. You shouldn't need to +// verify this in practice, but we found it useful during our own debugging. +using person_sensor_results_t = struct __attribute__((__packed__)) { + person_sensor_results_header_t header; // Bytes 0-4. + int8_t num_faces; // Byte 5. + person_sensor_face_t faces[PERSON_SENSOR_MAX_FACES_COUNT]; // Bytes 6-37. + uint16_t checksum; // Bytes 38-39. +}; + +class Sen21231Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void update() override; + void dump_config() override; + + protected: + void read_data_(); +}; + +} // namespace sen21231_sensor +} // namespace esphome diff --git a/esphome/components/sen21231/sensor.py b/esphome/components/sen21231/sensor.py new file mode 100644 index 0000000000..fb1dc19278 --- /dev/null +++ b/esphome/components/sen21231/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ICON_MOTION_SENSOR + +CODEOWNERS = ["@shreyaskarnik"] +DEPENDENCIES = ["i2c"] + +sen21231_sensor_ns = cg.esphome_ns.namespace("sen21231_sensor") +Sen21231Sensor = sen21231_sensor_ns.class_( + "Sen21231Sensor", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema(Sen21231Sensor, icon=ICON_MOTION_SENSOR, accuracy_decimals=1) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x62)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index bdc6da3406..d43490c8cc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1221,7 +1221,9 @@ sensor: name: "Still Energy" detection_distance: name: "Distance Detection" - + - platform: sen21231 + name: "Person Sensor" + i2c_id: i2c_bus esp32_touch: setup_mode: false iir_filter: 10ms From f98d93efa8b754210e931943eb8ffc5f9c174646 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:38:45 +1300 Subject: [PATCH 054/115] Fix multiple remote_receivers with triggers (#4477) --- esphome/components/remote_base/__init__.py | 9 ++++++--- esphome/components/remote_receiver/__init__.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index c3149ce430..c5c8921e20 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -79,7 +79,9 @@ def register_trigger(name, type, data_type): validator = automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + cv.Optional(CONF_RECEIVER_ID): cv.invalid( + "This has been removed in ESPHome 2022.3.0 and the trigger attaches directly to the parent receiver." + ), } ) registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator) @@ -87,7 +89,6 @@ def register_trigger(name, type, data_type): def decorator(func): async def new_func(config): var = cg.new_Pvariable(config[CONF_TRIGGER_ID]) - await register_listener(var, config) await coroutine(func)(var, config) await automation.build_automation(var, [(data_type, "x")], config) return var @@ -223,10 +224,12 @@ async def build_binary_sensor(full_config): async def build_triggers(full_config): + triggers = [] for key in TRIGGER_REGISTRY: for config in full_config.get(key, []): func = TRIGGER_REGISTRY[key][0] - await func(config) + triggers.append(await func(config)) + return triggers async def build_dumpers(config): diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 253204bd1a..1ed9161ec7 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -56,7 +56,9 @@ async def to_code(config): for dumper in dumpers: cg.add(var.register_dumper(dumper)) - await remote_base.build_triggers(config) + triggers = await remote_base.build_triggers(config) + for trigger in triggers: + cg.add(var.register_listener(trigger)) await cg.register_component(var, config) cg.add(var.set_tolerance(config[CONF_TOLERANCE])) From 23f47d0ad29a3641217020a64b0ccef0aed6a605 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Feb 2023 14:22:39 +1300 Subject: [PATCH 055/115] Initial stab at importing idf components (#4000) * Initial stab at importing idf components * Handle repo with multiple components Allow components directly from yaml * Actually use the refresh config var * Update esphome/components/esp32/__init__.py --- esphome/components/esp32/__init__.py | 86 ++++++++++++++++++- esphome/components/esp32/const.py | 5 ++ .../external_components/__init__.py | 74 ++-------------- esphome/config_validation.py | 63 ++++++++++++++ esphome/const.py | 3 + 5 files changed, 163 insertions(+), 68 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f30fa9a7b2..0f16fc9293 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,29 +4,43 @@ from pathlib import Path import logging import os -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.const import ( CONF_BOARD, + CONF_COMPONENTS, CONF_FRAMEWORK, + CONF_NAME, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, CONF_VERSION, CONF_ADVANCED, + CONF_REFRESH, + CONF_PATH, + CONF_URL, + CONF_REF, CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + TYPE_GIT, + TYPE_LOCAL, __version__, ) -from esphome.core import CORE, HexInt +from esphome.core import CORE, HexInt, TimePeriod import esphome.config_validation as cv import esphome.codegen as cg +from esphome import git from .const import ( # noqa KEY_BOARD, + KEY_COMPONENTS, KEY_ESP32, + KEY_PATH, + KEY_REF, + KEY_REFRESH, + KEY_REPO, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, VARIANT_ESP32C3, @@ -51,6 +65,7 @@ def set_core_data(config): if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf" CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} + CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( @@ -104,6 +119,21 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value +def add_idf_component( + name: str, repo: str, ref: str = None, path: str = None, refresh: TimePeriod = None +): + """Add an esp-idf component to the project.""" + if not CORE.using_esp_idf: + raise ValueError("Not an esp-idf project") + if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: + CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: path, + KEY_REFRESH: refresh, + } + + def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to # a PIO platformio/framework-arduinoespressif32 value @@ -270,6 +300,18 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, } ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ) + ), } ), _esp_idf_check_versions, @@ -372,6 +414,19 @@ async def to_code(config): ), ) + for component in conf[CONF_COMPONENTS]: + source = component[CONF_SOURCE] + if source[CONF_TYPE] == TYPE_GIT: + add_idf_component( + name=component[CONF_NAME], + repo=source[CONF_URL], + ref=source.get(CONF_REF), + path=component.get(CONF_PATH), + refresh=component[CONF_REFRESH], + ) + elif source[CONF_TYPE] == TYPE_LOCAL: + _LOGGER.warning("Local components are not implemented yet.") + elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") @@ -468,6 +523,33 @@ def copy_files(): __version__, ) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: + import shutil + + shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) + + components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] + + for name, component in components.items(): + + repo_dir, _ = git.clone_or_update( + url=component[KEY_REPO], + ref=component[KEY_REF], + refresh=component[KEY_REFRESH], + domain="idf_components", + ) + mkdir_p(CORE.relative_build_path("components")) + component_dir = repo_dir + if component[KEY_PATH] is not None: + component_dir = component_dir / component[KEY_PATH] + + shutil.copytree( + component_dir, + CORE.relative_build_path(f"components/{name}"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git", ".github"), + ) + 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/esp32/const.py b/esphome/components/esp32/const.py index d92b449ee9..d13df01d3a 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -4,6 +4,11 @@ KEY_ESP32 = "esp32" KEY_BOARD = "board" KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" +KEY_COMPONENTS = "components" +KEY_REPO = "repo" +KEY_REF = "ref" +KEY_REFRESH = "refresh" +KEY_PATH = "path" VARIANT_ESP32 = "ESP32" VARIANT_ESP32S2 = "ESP32S2" diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 53fd337ed8..bbb703dc5c 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/config_validation.py b/esphome/config_validation.py index 7440f71790..a46d14053b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -39,6 +39,11 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE, + CONF_REF, + CONF_URL, + CONF_PATH, + CONF_USERNAME, + CONF_PASSWORD, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_NONE, @@ -46,6 +51,8 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + TYPE_GIT, + TYPE_LOCAL, ) from esphome.core import ( CORE, @@ -1820,3 +1827,59 @@ def suppress_invalid(): yield except vol.Invalid: pass + + +GIT_SCHEMA = { + Required(CONF_URL): url, + Optional(CONF_REF): git_ref, + Optional(CONF_USERNAME): string, + Optional(CONF_PASSWORD): string, +} +LOCAL_SCHEMA = { + Required(CONF_PATH): directory, +} + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise Invalid("Shorthand only for strings") + try: + return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except 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 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 = Any( + validate_source_shorthand, + typed_schema( + { + TYPE_GIT: Schema(GIT_SCHEMA), + TYPE_LOCAL: Schema(LOCAL_SCHEMA), + } + ), +) diff --git a/esphome/const.py b/esphome/const.py index bd593bbc80..d821a76b75 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -797,6 +797,9 @@ CONF_X_GRID = "x_grid" CONF_Y_GRID = "y_grid" CONF_ZERO = "zero" +TYPE_GIT = "git" +TYPE_LOCAL = "local" + ENV_NOGITIGNORE = "ESPHOME_NOGITIGNORE" ENV_QUICKWIZARD = "ESPHOME_QUICKWIZARD" From 350d4e5071c21ed3acc0fb6b4c6d571dfc35efd9 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 22 Feb 2023 17:31:35 -0800 Subject: [PATCH 056/115] add kuntze component (#4411) * add kuntze component * fixes * more lint --------- Co-authored-by: Samuel Sieb --- CODEOWNERS | 1 + esphome/components/kuntze/__init__.py | 0 esphome/components/kuntze/kuntze.cpp | 91 +++++++++++++++++++ esphome/components/kuntze/kuntze.h | 42 +++++++++ esphome/components/kuntze/sensor.py | 123 ++++++++++++++++++++++++++ tests/test3.yaml | 6 ++ tests/test5.yaml | 6 ++ 7 files changed, 269 insertions(+) create mode 100644 esphome/components/kuntze/__init__.py create mode 100644 esphome/components/kuntze/kuntze.cpp create mode 100644 esphome/components/kuntze/kuntze.h create mode 100644 esphome/components/kuntze/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d24a19adb1..00f207862a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb +esphome/components/kuntze/* @ssieb esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @sebcaps esphome/components/ledc/* @OttoWinter diff --git a/esphome/components/kuntze/__init__.py b/esphome/components/kuntze/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp new file mode 100644 index 0000000000..e50dafca86 --- /dev/null +++ b/esphome/components/kuntze/kuntze.cpp @@ -0,0 +1,91 @@ +#include "kuntze.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace kuntze { + +static const char *const TAG = "kuntze"; + +static const uint8_t CMD_READ_REG = 0x03; +static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832}; + +void Kuntze::on_modbus_data(const std::vector &data) { + auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); }; + + this->waiting_ = false; + ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str()); + + float value = (float) get_16bit(0); + for (int i = 0; i < data[3]; i++) + value /= 10.0; + switch (this->state_) { + case 1: + ESP_LOGD(TAG, "pH=%.1f", value); + if (this->ph_sensor_ != nullptr) + this->ph_sensor_->publish_state(value); + break; + case 2: + ESP_LOGD(TAG, "temperature=%.1f", value); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(value); + break; + case 3: + ESP_LOGD(TAG, "DIS1=%.1f", value); + if (this->dis1_sensor_ != nullptr) + this->dis1_sensor_->publish_state(value); + break; + case 4: + ESP_LOGD(TAG, "DIS2=%.1f", value); + if (this->dis2_sensor_ != nullptr) + this->dis2_sensor_->publish_state(value); + break; + case 5: + ESP_LOGD(TAG, "REDOX=%.1f", value); + if (this->redox_sensor_ != nullptr) + this->redox_sensor_->publish_state(value); + break; + case 6: + ESP_LOGD(TAG, "EC=%.1f", value); + if (this->ec_sensor_ != nullptr) + this->ec_sensor_->publish_state(value); + break; + case 7: + ESP_LOGD(TAG, "OCI=%.1f", value); + if (this->oci_sensor_ != nullptr) + this->oci_sensor_->publish_state(value); + break; + } + if (++this->state_ > 7) + this->state_ = 0; +} + +void Kuntze::loop() { + uint32_t now = millis(); + // timeout after 15 seconds + if (this->waiting_ && (now - this->last_send_ > 15000)) { + ESP_LOGW(TAG, "timed out waiting for response"); + this->waiting_ = false; + } + if (this->waiting_ || (this->state_ == 0)) + return; + this->last_send_ = now; + send(CMD_READ_REG, REGISTER[this->state_ - 1], 2); + this->waiting_ = true; +} + +void Kuntze::update() { this->state_ = 1; } + +void Kuntze::dump_config() { + ESP_LOGCONFIG(TAG, "Kuntze:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "pH", this->ph_sensor_); + LOG_SENSOR("", "temperature", this->temperature_sensor_); + LOG_SENSOR("", "DIS1", this->dis1_sensor_); + LOG_SENSOR("", "DIS2", this->dis2_sensor_); + LOG_SENSOR("", "REDOX", this->redox_sensor_); + LOG_SENSOR("", "EC", this->ec_sensor_); + LOG_SENSOR("", "OCI", this->oci_sensor_); +} + +} // namespace kuntze +} // namespace esphome diff --git a/esphome/components/kuntze/kuntze.h b/esphome/components/kuntze/kuntze.h new file mode 100644 index 0000000000..aad7c1cbbf --- /dev/null +++ b/esphome/components/kuntze/kuntze.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace kuntze { + +class Kuntze : public PollingComponent, public modbus::ModbusDevice { + public: + void set_ph_sensor(sensor::Sensor *ph_sensor) { ph_sensor_ = ph_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_dis1_sensor(sensor::Sensor *dis1_sensor) { dis1_sensor_ = dis1_sensor; } + void set_dis2_sensor(sensor::Sensor *dis2_sensor) { dis2_sensor_ = dis2_sensor; } + void set_redox_sensor(sensor::Sensor *redox_sensor) { redox_sensor_ = redox_sensor; } + void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; } + void set_oci_sensor(sensor::Sensor *oci_sensor) { oci_sensor_ = oci_sensor; } + + void loop() override; + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + int state_{0}; + bool waiting_{false}; + uint32_t last_send_{0}; + + sensor::Sensor *ph_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *dis1_sensor_{nullptr}; + sensor::Sensor *dis2_sensor_{nullptr}; + sensor::Sensor *redox_sensor_{nullptr}; + sensor::Sensor *ec_sensor_{nullptr}; + sensor::Sensor *oci_sensor_{nullptr}; +}; + +} // namespace kuntze +} // namespace esphome diff --git a/esphome/components/kuntze/sensor.py b/esphome/components/kuntze/sensor.py new file mode 100644 index 0000000000..96c874fa5c --- /dev/null +++ b/esphome/components/kuntze/sensor.py @@ -0,0 +1,123 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ID, + CONF_EC, + CONF_PH, + CONF_TEMPERATURE, + ICON_EMPTY, + ICON_THERMOMETER, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_PH, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_TEMPERATURE, +) + +CODEOWNERS = ["@ssieb"] + +AUTO_LOAD = ["modbus"] + +kuntze_ns = cg.esphome_ns.namespace("kuntze") +Kuntze = kuntze_ns.class_("Kuntze", cg.PollingComponent, modbus.ModbusDevice) + +CONF_DIS1 = "dis1" +CONF_DIS2 = "dis2" +CONF_REDOX = "redox" +CONF_OCI = "oci" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Kuntze), + cv.Optional(CONF_PH): sensor.sensor_schema( + unit_of_measurement=UNIT_PH, + icon=ICON_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + cv.Optional(CONF_DIS1): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_DIS2): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_REDOX): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_EC): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_OCI): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + icon=ICON_EMPTY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_EMPTY, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + + if CONF_PH in config: + conf = config[CONF_PH] + sens = await sensor.new_sensor(conf) + cg.add(var.set_ph_sensor(sens)) + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temperature_sensor(sens)) + if CONF_DIS1 in config: + conf = config[CONF_DIS1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_dis1_sensor(sens)) + if CONF_DIS2 in config: + conf = config[CONF_DIS2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_dis2_sensor(sens)) + if CONF_REDOX in config: + conf = config[CONF_REDOX] + sens = await sensor.new_sensor(conf) + cg.add(var.set_redox_sensor(sens)) + if CONF_EC in config: + conf = config[CONF_EC] + sens = await sensor.new_sensor(conf) + cg.add(var.set_ec_sensor(sens)) + if CONF_OCI in config: + conf = config[CONF_OCI] + sens = await sensor.new_sensor(conf) + cg.add(var.set_oci_sensor(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index 4827b7cbcd..6755be9f14 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -807,6 +807,12 @@ sensor: temperature_1: name: Temperature 1 + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature + time: - platform: homeassistant diff --git a/tests/test5.yaml b/tests/test5.yaml index 21419692e4..5708f1f093 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -520,6 +520,12 @@ sensor: name: VBus Custom Sensor lambda: return x[0] / 10.0; + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature + script: - id: automation_test then: From fe4fb5f1ac796180c25711da2af624f5b2e8c663 Mon Sep 17 00:00:00 2001 From: Yaroslav Heriatovych <184247+Yarikx@users.noreply.github.com> Date: Thu, 23 Feb 2023 02:05:33 +0000 Subject: [PATCH 057/115] Add Haier climate component (#4001) * Basic functionality works * Cleanup * Add tests * Separate header * Fix send_data_ * Formatting fix * Add __init__.py * Fix type * Add codeowners * Rename supported_swing_modes * Use multiple swing modes, same as midea platform * Add CLIMATE_FAN_QUIET handler * PR fixes --- CODEOWNERS | 1 + esphome/components/haier/__init__.py | 1 + esphome/components/haier/climate.py | 43 ++++ esphome/components/haier/haier.cpp | 302 +++++++++++++++++++++++++++ esphome/components/haier/haier.h | 37 ++++ tests/test3.yaml | 14 +- 6 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 esphome/components/haier/__init__.py create mode 100644 esphome/components/haier/climate.py create mode 100644 esphome/components/haier/haier.cpp create mode 100644 esphome/components/haier/haier.h diff --git a/CODEOWNERS b/CODEOWNERS index 00f207862a..4e3c6c3510 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,7 @@ esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco esphome/components/growatt_solar/* @leeuwte +esphome/components/haier/* @Yarikx esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann diff --git a/esphome/components/haier/__init__.py b/esphome/components/haier/__init__.py new file mode 100644 index 0000000000..b9ea055a41 --- /dev/null +++ b/esphome/components/haier/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Yarikx"] diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py new file mode 100644 index 0000000000..cee83232a1 --- /dev/null +++ b/esphome/components/haier/climate.py @@ -0,0 +1,43 @@ +from esphome.components import climate +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.components.climate import ClimateSwingMode +from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES + +DEPENDENCIES = ["uart"] + +haier_ns = cg.esphome_ns.namespace("haier") +HaierClimate = haier_ns.class_( + "HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice +) + +ALLOWED_CLIMATE_SWING_MODES = { + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HaierClimate), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( + validate_swing_modes + ), + } + ) + .extend(cv.polling_component_schema("5s")) + .extend(uart.UART_DEVICE_SCHEMA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await uart.register_uart_device(var, config) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) diff --git a/esphome/components/haier/haier.cpp b/esphome/components/haier/haier.cpp new file mode 100644 index 0000000000..cf69d483b5 --- /dev/null +++ b/esphome/components/haier/haier.cpp @@ -0,0 +1,302 @@ +#include +#include "haier.h" +#include "esphome/core/macros.h" + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier"; + +static const uint8_t TEMPERATURE = 13; +static const uint8_t HUMIDITY = 15; + +static const uint8_t MODE = 23; + +static const uint8_t FAN_SPEED = 25; + +static const uint8_t SWING = 27; + +static const uint8_t POWER = 29; +static const uint8_t POWER_MASK = 1; + +static const uint8_t SET_TEMPERATURE = 35; +static const uint8_t DECIMAL_MASK = (1 << 5); + +static const uint8_t CRC = 36; + +static const uint8_t COMFORT_PRESET_MASK = (1 << 3); + +static const uint8_t MIN_VALID_TEMPERATURE = 16; +static const uint8_t MAX_VALID_TEMPERATURE = 50; +static const float TEMPERATURE_STEP = 0.5f; + +static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90}; +static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92}; + +void HaierClimate::dump_config() { + ESP_LOGCONFIG(TAG, "Haier:"); + ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval()); + this->dump_traits_(TAG); + this->check_uart_settings(9600); +} + +void HaierClimate::loop() { + if (this->available() >= sizeof(this->data_)) { + this->read_array(this->data_, sizeof(this->data_)); + if (this->data_[0] != 255 || this->data_[1] != 255) + return; + + read_state_(this->data_, sizeof(this->data_)); + } +} + +void HaierClimate::update() { + this->write_array(POLL_REQ, sizeof(POLL_REQ)); + dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ)); +} + +climate::ClimateTraits HaierClimate::traits() { + auto traits = climate::ClimateTraits(); + + traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE); + traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE); + traits.set_visual_temperature_step(TEMPERATURE_STEP); + + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY}); + + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + }); + + traits.set_supported_swing_modes(this->supported_swing_modes_); + traits.set_supports_current_temperature(true); + traits.set_supports_two_point_target_temperature(false); + + traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); + traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT); + + return traits; +} + +void HaierClimate::read_state_(const uint8_t *data, uint8_t size) { + dump_message_("Received state", data, size); + + uint8_t check = data[CRC]; + + uint8_t crc = get_checksum_(data, size); + + if (check != crc) { + ESP_LOGW(TAG, "Invalid checksum"); + return; + } + + this->current_temperature = data[TEMPERATURE]; + + this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE; + + if (data[POWER] & DECIMAL_MASK) { + this->target_temperature += 0.5f; + } + + switch (data[MODE]) { + case MODE_SMART: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case MODE_ONLY_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + default: // other modes are unsupported + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + } + + switch (data[FAN_SPEED]) { + case FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + + case FAN_MIN: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + + case FAN_MIDDLE: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + + case FAN_MAX: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + } + + switch (data[SWING]) { + case SWING_OFF: + this->swing_mode = climate::CLIMATE_SWING_OFF; + break; + + case SWING_VERTICAL: + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + break; + + case SWING_HORIZONTAL: + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + break; + + case SWING_BOTH: + this->swing_mode = climate::CLIMATE_SWING_BOTH; + break; + } + + if (data[POWER] & COMFORT_PRESET_MASK) { + this->preset = climate::CLIMATE_PRESET_COMFORT; + } else { + this->preset = climate::CLIMATE_PRESET_NONE; + } + + if ((data[POWER] & POWER_MASK) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } + + this->publish_state(); +} + +void HaierClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) { + switch (call.get_mode().value()) { + case climate::CLIMATE_MODE_OFF: + send_data_(OFF_REQ, sizeof(OFF_REQ)); + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_AUTO: + data_[POWER] |= POWER_MASK; + data_[MODE] = MODE_SMART; + break; + case climate::CLIMATE_MODE_HEAT: + data_[POWER] |= POWER_MASK; + data_[MODE] = MODE_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + data_[POWER] |= POWER_MASK; + data_[MODE] = MODE_COOL; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + data_[POWER] |= POWER_MASK; + data_[MODE] = MODE_ONLY_FAN; + break; + + case climate::CLIMATE_MODE_DRY: + data_[POWER] |= POWER_MASK; + data_[MODE] = MODE_DRY; + break; + } + } + + if (call.get_preset().has_value()) { + if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) { + data_[POWER] |= COMFORT_PRESET_MASK; + } else { + data_[POWER] &= ~COMFORT_PRESET_MASK; + } + } + + if (call.get_target_temperature().has_value()) { + float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE; + + data_[SET_TEMPERATURE] = (uint8_t) target; + + if ((int) target == std::lroundf(target)) { + data_[POWER] &= ~DECIMAL_MASK; + } else { + data_[POWER] |= DECIMAL_MASK; + } + } + + if (call.get_fan_mode().has_value()) { + switch (call.get_fan_mode().value()) { + case climate::CLIMATE_FAN_AUTO: + data_[FAN_SPEED] = FAN_AUTO; + break; + case climate::CLIMATE_FAN_LOW: + data_[FAN_SPEED] = FAN_MIN; + break; + case climate::CLIMATE_FAN_MEDIUM: + data_[FAN_SPEED] = FAN_MIDDLE; + break; + case climate::CLIMATE_FAN_HIGH: + data_[FAN_SPEED] = FAN_MAX; + break; + + default: // other modes are unsupported + break; + } + } + + if (call.get_swing_mode().has_value()) { + switch (call.get_swing_mode().value()) { + case climate::CLIMATE_SWING_OFF: + data_[SWING] = SWING_OFF; + break; + case climate::CLIMATE_SWING_VERTICAL: + data_[SWING] = SWING_VERTICAL; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + data_[SWING] = SWING_HORIZONTAL; + break; + case climate::CLIMATE_SWING_BOTH: + data_[SWING] = SWING_BOTH; + break; + } + } + + // Parts of the message that must have specific values for "send" command. + // The meaning of those values is unknown at the moment. + data_[9] = 1; + data_[10] = 77; + data_[11] = 95; + data_[17] = 0; + + // Compute checksum + uint8_t crc = get_checksum_(data_, sizeof(data_)); + data_[CRC] = crc; + + send_data_(data_, sizeof(data_)); +} + +void HaierClimate::send_data_(const uint8_t *message, uint8_t size) { + this->write_array(message, size); + + dump_message_("Sent message", message, size); +} + +void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) { + ESP_LOGV(TAG, "%s:", title); + for (int i = 0; i < size; i++) { + ESP_LOGV(TAG, " byte %02d - %d", i, message[i]); + } +} + +uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) { + uint8_t position = size - 1; + uint8_t crc = 0; + + for (int i = 2; i < position; i++) + crc += message[i]; + + return crc; +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/haier.h b/esphome/components/haier/haier.h new file mode 100644 index 0000000000..5399fd187b --- /dev/null +++ b/esphome/components/haier/haier.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace haier { + +enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 }; +enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 }; +enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 }; + +class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent { + public: + void loop() override; + void update() override; + void dump_config() override; + void control(const climate::ClimateCall &call) override; + void set_supported_swing_modes(const std::set &modes) { + this->supported_swing_modes_ = modes; + } + + protected: + climate::ClimateTraits traits() override; + void read_state_(const uint8_t *data, uint8_t size); + void send_data_(const uint8_t *message, uint8_t size); + void dump_message_(const char *title, const uint8_t *message, uint8_t size); + uint8_t get_checksum_(const uint8_t *message, size_t size); + + private: + uint8_t data_[37]; + std::set supported_swing_modes_{}; +}; + +} // namespace haier +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 6755be9f14..16489335af 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -283,6 +283,10 @@ uart: tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 + - id: uart12 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 modbus: uart_id: uart1 @@ -1194,8 +1198,14 @@ climate: ki_multiplier: 0.0 kd_multiplier: 0.0 deadband_output_averaging_samples: 1 - - + - platform: haier + name: Haier AC + supported_swing_modes: + - vertical + - horizontal + - both + update_interval: 10s + uart_id: uart12 sprinkler: - id: yard_sprinkler_ctrlr From 6704b2cedfd310ba6a62f8ae96d59ea9d8e3a01a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Feb 2023 21:08:06 -0500 Subject: [PATCH 058/115] Bump esp-idf to 4.4.3 via platformio/espressif32 @ 5.3.0 (#4254) * Bump esp-idf to 3.4.3 via platformio/espressif32 @ 5.3.0 The new version appears to improve the stability of BLE + WiFi * bump recommended version as well --- esphome/components/esp32/__init__.py | 4 ++-- platformio.ini | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0f16fc9293..0566d12f97 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -168,11 +168,11 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 2, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 2) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 3) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(5, 2, 0) +ESP_IDF_PLATFORM_VERSION = cv.Version(5, 3, 0) def _arduino_check_versions(value): diff --git a/platformio.ini b/platformio.ini index ef5820c4ad..f2588e918a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = platformio/espressif32 @ 5.2.0 +platform = platformio/espressif32 @ 5.3.0 platform_packages = - platformio/framework-espidf @ ~3.40402.0 + platformio/framework-espidf @ ~3.40403.0 framework = espidf lib_deps = From 4d674392e8953cbb530c318f6d931d89f50ac5c4 Mon Sep 17 00:00:00 2001 From: Andreas Hergert <36455093+andreashergert1984@users.noreply.github.com> Date: Thu, 23 Feb 2023 18:38:34 +0100 Subject: [PATCH 059/115] Add energy to pzemdc (#3626) * added energy to pzemdc * fixed calculation * added test * moved tests --------- Co-authored-by: Andreas Hergert --- esphome/components/pzemdc/pzemdc.cpp | 6 ++++++ esphome/components/pzemdc/pzemdc.h | 2 ++ esphome/components/pzemdc/sensor.py | 14 ++++++++++++++ tests/test3.yaml | 2 ++ tests/test5.yaml | 1 - 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 6a31a723a1..c914edefc8 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -37,6 +37,9 @@ void PZEMDC::on_modbus_data(const std::vector &data) { uint32_t raw_power = pzem_get_32bit(4); float power = raw_power / 10.0f; // max 429496729.5 W + uint32_t raw_energy = pzem_get_32bit(8); + float energy = raw_energy / 1000.0f; // max 4294967.295 kWh + ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power); if (this->voltage_sensor_ != nullptr) this->voltage_sensor_->publish_state(voltage); @@ -44,6 +47,8 @@ void PZEMDC::on_modbus_data(const std::vector &data) { this->current_sensor_->publish_state(current); if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(power); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(energy); } void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } @@ -53,6 +58,7 @@ void PZEMDC::dump_config() { LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Energy", this->energy_sensor_); } } // namespace pzemdc diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index dff904476b..54df6c484c 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -14,6 +14,7 @@ class PZEMDC : public PollingComponent, public modbus::ModbusDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } @@ -29,6 +30,7 @@ class PZEMDC : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *frequency_sensor_{nullptr}; sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *energy_sensor_{nullptr}; }; } // namespace pzemdc diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 08ec688afb..4234ce19ad 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -5,14 +5,18 @@ from esphome.const import ( CONF_CURRENT, CONF_ID, CONF_POWER, + CONF_ENERGY, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, + UNIT_KILOWATT_HOURS, ) AUTO_LOAD = ["modbus"] @@ -42,6 +46,12 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), } ) .extend(cv.polling_component_schema("60s")) @@ -66,3 +76,7 @@ async def to_code(config): conf = config[CONF_POWER] sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index 16489335af..5776e1fc10 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -583,6 +583,8 @@ sensor: name: PZEMDC Current power: name: PZEMDC Power + energy: + name: PZEMDC Energy - platform: tmp102 name: TMP102 Temperature - platform: hm3301 diff --git a/tests/test5.yaml b/tests/test5.yaml index 5708f1f093..ec3fef5e19 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -477,7 +477,6 @@ sensor: acceleration_mode: low store_baseline: true address: 0x69 - - platform: mcp9600 thermocouple_type: K hot_junction: From 6ec18fc63056a8d9a5636268ec1b1eaf3f2914e7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Feb 2023 07:25:22 +1300 Subject: [PATCH 060/115] Update esp32 esp-idf dev and latest version numbers (#4479) --- esphome/components/esp32/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0566d12f97..69fc8d3262 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -178,8 +178,8 @@ ESP_IDF_PLATFORM_VERSION = cv.Version(5, 3, 0) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 0, 5), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(2, 0, 5), None), + "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(2, 0, 7), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -213,8 +213,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 0, 0), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(4, 4, 2), None), + "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 0, 1), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } @@ -531,7 +531,6 @@ def copy_files(): components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] for name, component in components.items(): - repo_dir, _ = git.clone_or_update( url=component[KEY_REPO], ref=component[KEY_REF], From 1a9141877d261334393183dfe66e372e9d67832b Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 26 Feb 2023 19:42:29 +0100 Subject: [PATCH 061/115] use same `heap_caps_malloc` parameter as `ps_malloc`. (#4484) Co-authored-by: Your Name Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 477dadc187..cef303941e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -612,7 +612,7 @@ template class ExternalRAMAllocator { size_t size = n * sizeof(T); T *ptr = nullptr; #ifdef USE_ESP32 - ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)); + ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); #endif if (ptr == nullptr && (this->flags_ & Flags::REFUSE_INTERNAL) == 0) ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) From 86c0e6114f3a8babd3d1217fbc0cc33380ce0ae5 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 26 Feb 2023 19:43:08 +0100 Subject: [PATCH 062/115] Lock scheduler items while modifying them (#4410) * Cosmetic fixes to scheduler code * Add generic Mutex API * Lock scheduler items while modifying them * Always defer MQTT callbacks on Arduino --- esphome/components/mqtt/mqtt_client.cpp | 8 ++--- esphome/core/helpers.cpp | 12 +++++++ esphome/core/helpers.h | 41 ++++++++++++++++++++++ esphome/core/scheduler.cpp | 46 +++++++++++++++++++++---- esphome/core/scheduler.h | 5 ++- 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index aa0cf56c51..acb863244e 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -485,16 +485,16 @@ static bool topic_match(const char *message, const char *subscription) { } void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { -#ifdef USE_ESP8266 - // on ESP8266, this is called in LWiP thread; some components do not like running - // in an ISR. +#ifdef USE_ARDUINO + // on Arduino, this is called in lwIP/AsyncTCP task; some components do not like running + // from a different task. this->defer([this, topic, payload]() { #endif for (auto &subscription : this->subscriptions_) { if (topic_match(topic.c_str(), subscription.topic.c_str())) subscription.callback(topic, payload); } -#ifdef USE_ESP8266 +#ifdef USE_ARDUINO }); #endif } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 7ec5d9d23c..4ac9303b20 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -393,6 +393,18 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green } // System APIs +#if defined(USE_ESP8266) +// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. +Mutex::Mutex() {} +void Mutex::lock() {} +bool Mutex::try_lock() { return true; } +void Mutex::unlock() {} +#elif defined(USE_ESP32) || defined(USE_RP2040) +Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } +void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } +bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } +void Mutex::unlock() { xSemaphoreGive(this->handle_); } +#endif #if defined(USE_ESP8266) IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index cef303941e..0d2a7e298a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -14,6 +14,14 @@ #include #endif +#if defined(USE_ESP32) +#include +#include +#elif defined(USE_RP2040) +#include +#include +#endif + #define HOT __attribute__((hot)) #define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg))) #define ALWAYS_INLINE __attribute__((always_inline)) @@ -516,6 +524,39 @@ template class Parented { /// @name System APIs ///@{ +/** Mutex implementation, with API based on the unavailable std::mutex. + * + * @note This mutex is non-recursive, so take care not to try to obtain the mutex while it is already taken. + */ +class Mutex { + public: + Mutex(); + Mutex(const Mutex &) = delete; + void lock(); + bool try_lock(); + void unlock(); + + Mutex &operator=(const Mutex &) = delete; + + private: +#if defined(USE_ESP32) || defined(USE_RP2040) + SemaphoreHandle_t handle_; +#endif +}; + +/** Helper class that wraps a mutex with a RAII-style API. + * + * This behaves like std::lock_guard: as long as the object is alive, the mutex is held. + */ +class LockGuard { + public: + LockGuard(Mutex &mutex) : mutex_{mutex} { mutex_.lock(); } + ~LockGuard() { mutex_.unlock(); } + + private: + Mutex &mutex_; +}; + /** Helper class to disable interrupts. * * This behaves like std::lock_guard: as long as the object is alive, all interrupts are disabled. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d880f0fda4..0cb148ec13 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -13,6 +13,11 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER +// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to +// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, +// iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to +// avoid the main thread modifying the list while it is being accessed. + void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func) { const uint32_t now = this->millis_(); @@ -121,7 +126,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin args->backoff_increase_factor = backoff_increase_factor; args->scheduler = this; - // First exectuion of `func` immediately + // First execution of `func` immediately this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); }); } bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { @@ -150,30 +155,42 @@ void HOT Scheduler::call() { std::vector> old_items; ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); while (!this->empty_()) { + this->lock_.lock(); auto item = std::move(this->items_[0]); + this->pop_raw_(); + this->lock_.unlock(); + ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(), item->name.c_str(), item->interval, item->last_execution, item->last_execution_major, item->next_execution(), item->next_execution_major()); - this->pop_raw_(); old_items.push_back(std::move(item)); } ESP_LOGVV(TAG, "\n"); - this->items_ = std::move(old_items); + + { + LockGuard guard{this->lock_}; + this->items_ = std::move(old_items); + } } #endif // ESPHOME_DEBUG_SCHEDULER auto to_remove_was = to_remove_; - auto items_was = items_.size(); + auto items_was = this->items_.size(); // If we have too many items to remove if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { std::vector> valid_items; while (!this->empty_()) { + LockGuard guard{this->lock_}; auto item = std::move(this->items_[0]); this->pop_raw_(); valid_items.push_back(std::move(item)); } - this->items_ = std::move(valid_items); + + { + LockGuard guard{this->lock_}; + this->items_ = std::move(valid_items); + } // The following should not happen unless I'm missing something if (to_remove_ != 0) { @@ -198,6 +215,7 @@ void HOT Scheduler::call() { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { + LockGuard guard{this->lock_}; this->pop_raw_(); continue; } @@ -217,6 +235,8 @@ void HOT Scheduler::call() { } { + this->lock_.lock(); + // new scope, item from before might have been moved in the vector auto item = std::move(this->items_[0]); @@ -224,6 +244,8 @@ void HOT Scheduler::call() { // during the function call and know if we were cancelled. this->pop_raw_(); + this->lock_.unlock(); + if (item->remove) { // We were removed/cancelled in the function call, stop to_remove_--; @@ -246,6 +268,7 @@ void HOT Scheduler::call() { this->process_to_add(); } void HOT Scheduler::process_to_add() { + LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { if (it->remove) { continue; @@ -263,15 +286,24 @@ void HOT Scheduler::cleanup_() { return; to_remove_--; - this->pop_raw_(); + + { + LockGuard guard{this->lock_}; + this->pop_raw_(); + } } } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->items_.pop_back(); } -void HOT Scheduler::push_(std::unique_ptr item) { this->to_add_.push_back(std::move(item)); } +void HOT Scheduler::push_(std::unique_ptr item) { + LockGuard guard{this->lock_}; + this->to_add_.push_back(std::move(item)); +} bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { + // obtain lock because this function iterates and can be called from non-loop task context + LockGuard guard{this->lock_}; bool ret = false; for (auto &it : this->items_) { if (it->component == component && it->name == name && it->type == type && !it->remove) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index a758198b8d..44a58f37f5 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -1,9 +1,11 @@ #pragma once -#include "esphome/core/component.h" #include #include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + namespace esphome { class Component; @@ -71,6 +73,7 @@ class Scheduler { return this->items_.empty(); } + Mutex lock_; std::vector> items_; std::vector> to_add_; uint32_t last_millis_{0}; From 62459a8ae1d9a2182772d55ddf7d6ad983277f2e Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 26 Feb 2023 19:45:30 +0100 Subject: [PATCH 063/115] Move Font glyphs to SPI RAM. (#4485) Co-authored-by: Your Name Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/display/display_buffer.cpp | 19 +++++++++++++------ esphome/components/display/display_buffer.h | 6 ++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 9fe4137a14..85ebd2567b 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -256,7 +256,7 @@ void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align if (glyph_n < 0) { // Unknown char, skip ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!font->get_glyphs().empty()) { + if (font->get_glyphs_size() > 0) { uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width; for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) { for (int glyph_y = 0; glyph_y < height; glyph_y++) @@ -557,7 +557,7 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { } int Font::match_next_glyph(const char *str, int *match_length) { int lo = 0; - int hi = this->glyphs_.size() - 1; + int hi = this->glyphs_size_ - 1; while (lo != hi) { int mid = (lo + hi + 1) / 2; if (this->glyphs_[mid].compare_to(str)) { @@ -583,7 +583,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in int glyph_n = this->match_next_glyph(str + i, &match_length); if (glyph_n < 0) { // Unknown char, skip - if (!this->get_glyphs().empty()) + if (this->glyphs_size_ > 0) x += this->get_glyphs()[0].glyph_data_->width; i++; continue; @@ -603,10 +603,17 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *x_offset = min_x; *width = x - min_x; } -const std::vector &Font::get_glyphs() const { return this->glyphs_; } Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(data + i); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->glyphs_ = allocator.allocate(data_nr); + if (this->glyphs_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer for Glyphs!"); + return; + } + for (int i = 0; i < data_nr; ++i) { + this->glyphs_[i] = Glyph(data + i); + } + this->glyphs_size_ = data_nr; } bool Image::get_pixel(int x, int y) const { diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 3763da8041..815ba8d2e1 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -526,10 +526,12 @@ class Font { inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } - const std::vector &get_glyphs() const; + Glyph *&get_glyphs() { return this->glyphs_; } + const u_int16_t &get_glyphs_size() const { return this->glyphs_size_; } protected: - std::vector glyphs_; + Glyph *glyphs_{nullptr}; + u_int16_t glyphs_size_; int baseline_; int height_; }; From 14e7b8a1ef1895ccc751b718583025a4494918cf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Feb 2023 09:13:45 +1300 Subject: [PATCH 064/115] Run CI on merge group (#4489) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 782fc5cb32..60c987f6c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: branches: [dev, beta, release] pull_request: + merge_group: permissions: contents: read From 43fb68f8a087bbd2dd18736513ececa531568ea5 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 26 Feb 2023 23:23:04 +0100 Subject: [PATCH 065/115] Fix parallel invocations of repeat action (#4480) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/automation.py | 6 +++++- esphome/core/base_automation.h | 16 +++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 4aede00c5e..0c4bda09d1 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -254,7 +254,11 @@ async def repeat_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) cg.add(var.set_count(count_template)) - actions = await build_action_list(config[CONF_THEN], template_arg, args) + actions = await build_action_list( + config[CONF_THEN], + cg.TemplateArguments(cg.uint32, *template_arg.args), + [(cg.uint32, "iteration"), *args], + ) cg.add(var.add_then(actions)) return var diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index b36a64b82a..5a4fb65198 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -235,22 +235,21 @@ template class RepeatAction : public Action { public: TEMPLATABLE_VALUE(uint32_t, count) - void add_then(const std::vector *> &actions) { + void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { - this->iteration_++; - if (this->iteration_ == this->count_.value(x...)) + this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { + iteration++; + if (iteration >= this->count_.value(x...)) this->play_next_tuple_(this->var_); else - this->then_.play_tuple(this->var_); + this->then_.play(iteration, x...); })); } void play_complex(Ts... x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - this->iteration_ = 0; - this->then_.play_tuple(this->var_); + this->then_.play(0, x...); } void play(Ts... x) override { /* ignore - see play_complex */ @@ -259,8 +258,7 @@ template class RepeatAction : public Action { void stop() override { this->then_.stop(); } protected: - uint32_t iteration_; - ActionList then_; + ActionList then_; std::tuple var_; }; From eceb79ceabe381007d1714e3094ed229acba9219 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Feb 2023 11:34:51 +1300 Subject: [PATCH 066/115] Make test3 use huge_app (#4488) --- tests/test3.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test3.yaml b/tests/test3.yaml index 5776e1fc10..549d8f8557 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -3,6 +3,8 @@ esphome: name: $device_name comment: $device_comment build_path: build/test3 + platformio_options: + board_build.partitions: huge_app.csv5 on_boot: - if: condition: From 86407b9f6f95b24c798e4f64fe81b4ef5173fc89 Mon Sep 17 00:00:00 2001 From: GitforZhangXL <263866345@qq.com> Date: Mon, 27 Feb 2023 06:35:00 +0800 Subject: [PATCH 067/115] Change variable "skip_updates" and "skip_updates_counter" type from "uint8_t" to "uint16_t" (#4487) * change 'skip_updates'from 'uint8' to 'uint16_t' * Delete modbus_controller_fix.md * Update modbus_controller.h --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../modbus_controller/binary_sensor/modbus_binarysensor.h | 2 +- esphome/components/modbus_controller/modbus_controller.h | 8 ++++---- .../components/modbus_controller/number/modbus_number.h | 2 +- .../components/modbus_controller/select/modbus_select.h | 2 +- .../components/modbus_controller/sensor/modbus_sensor.h | 2 +- .../components/modbus_controller/switch/modbus_switch.h | 2 +- .../modbus_controller/text_sensor/modbus_textsensor.h | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index 3782416d4f..3a017c6f88 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -12,7 +12,7 @@ namespace modbus_controller { class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, public SensorItem { public: ModbusBinarySensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - uint8_t skip_updates, bool force_new_range) { + uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; this->offset = offset; diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 512fe0b25d..ccb0edf9c6 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -246,7 +246,7 @@ class SensorItem { uint8_t offset; uint8_t register_count; uint8_t response_bytes{0}; - uint8_t skip_updates; + uint16_t skip_updates; std::vector custom_data{}; bool force_new_range{false}; }; @@ -288,9 +288,9 @@ struct RegisterRange { uint16_t start_address; ModbusRegisterType register_type; uint8_t register_count; - uint8_t skip_updates; // the config value - SensorSet sensors; // all sensors of this range - uint8_t skip_updates_counter; // the running value + uint16_t skip_updates; // the config value + SensorSet sensors; // all sensors of this range + uint16_t skip_updates_counter; // the running value }; class ModbusCommandItem { diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 9b447d831c..544d161cbc 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -14,7 +14,7 @@ using value_to_data_t = std::function(float); class ModbusNumber : public number::Number, public Component, public SensorItem { public: ModbusNumber(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - SensorValueType value_type, int register_count, uint8_t skip_updates, bool force_new_range) { + SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; this->offset = offset; diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index ffbbba390b..1c046b11d0 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -12,7 +12,7 @@ namespace modbus_controller { class ModbusSelect : public Component, public select::Select, public SensorItem { public: - ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, uint8_t skip_updates, + ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, uint16_t skip_updates, bool force_new_range, std::vector mapping) { this->register_type = ModbusRegisterType::HOLDING; // not configurable this->sensor_value_type = sensor_value_type; diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 848d5f63de..65eb487c1c 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -12,7 +12,7 @@ namespace modbus_controller { class ModbusSensor : public Component, public sensor::Sensor, public SensorItem { public: ModbusSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - SensorValueType value_type, int register_count, uint8_t skip_updates, bool force_new_range) { + SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; this->offset = offset; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 0f2d8f6e59..bfe46f3ac8 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -12,7 +12,7 @@ namespace modbus_controller { class ModbusSwitch : public Component, public switch_::Switch, public SensorItem { public: ModbusSwitch(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - uint8_t skip_updates, bool force_new_range) { + uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; this->offset = offset; diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index 2e3be72034..9cc0db05a5 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -14,7 +14,7 @@ enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2 }; class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem { public: ModbusTextSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count, - uint16_t response_bytes, RawEncoding encode, uint8_t skip_updates, bool force_new_range) { + uint16_t response_bytes, RawEncoding encode, uint16_t skip_updates, bool force_new_range) { this->register_type = register_type; this->start_address = start_address; this->offset = offset; From a428e2b68950a85e3a8461e7270249f3d40449af Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:20:56 +1300 Subject: [PATCH 068/115] Fix copy-pasta mistake (#4492) --- tests/test3.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test3.yaml b/tests/test3.yaml index 549d8f8557..e62a171c95 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -4,7 +4,7 @@ esphome: comment: $device_comment build_path: build/test3 platformio_options: - board_build.partitions: huge_app.csv5 + board_build.partitions: huge_app.csv on_boot: - if: condition: From df3f13ded884682dfbcbba542d79aedb1e100b0a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 4 Mar 2023 20:19:51 +1300 Subject: [PATCH 069/115] Add int16 to codegen (#4507) --- esphome/codegen.py | 1 + esphome/cpp_types.py | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/codegen.py b/esphome/codegen.py index ef5b490004..c926c94070 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -63,6 +63,7 @@ from esphome.cpp_types import ( # noqa uint16, uint32, uint64, + int16, int32, int64, size_t, diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index aafe765111..7d0e386b66 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -14,6 +14,7 @@ uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") uint64 = global_ns.namespace("uint64_t") +int16 = global_ns.namespace("int16_t") int32 = global_ns.namespace("int32_t") int64 = global_ns.namespace("int64_t") size_t = global_ns.namespace("size_t") From bd86a0ac3b1f47306a9814cfd11c0374a61a33ab Mon Sep 17 00:00:00 2001 From: Aliasghar Dashkhaneh Date: Sun, 5 Mar 2023 22:15:54 +0330 Subject: [PATCH 070/115] Update __init__.py (#4514) In some Sony remote codes, the **data** is more than 16 bits. --- esphome/components/remote_base/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index c5c8921e20..4d9196c9c5 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -713,7 +713,7 @@ def sony_dumper(var, config): @register_action("sony", SonyAction, SONY_SCHEMA) async def sony_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16) + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) cg.add(var.set_nbits(template_)) From b8ca40170e59c08d0cd002c50c54db8ba86c792d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Mar 2023 09:17:32 +1300 Subject: [PATCH 071/115] Remove idf components before checking if any in config (#4506) * Remove idf components before checking if any in config * Fix bug with no refresh time specified --- esphome/components/esp32/__init__.py | 8 ++++---- esphome/git.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 69fc8d3262..ba11aeef21 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -523,11 +523,11 @@ def copy_files(): __version__, ) + import shutil + + shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: - import shutil - - shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) - components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] for name, component in components.items(): diff --git a/esphome/git.py b/esphome/git.py index 130cd4f5e1..a607325b73 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -44,7 +44,7 @@ def clone_or_update( *, url: str, ref: str = None, - refresh: TimePeriodSeconds, + refresh: Optional[TimePeriodSeconds], domain: str, username: str = None, password: str = None, @@ -81,7 +81,7 @@ def clone_or_update( if not file_timestamp.exists(): file_timestamp = Path(repo_dir / ".git" / "HEAD") age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) - if age.total_seconds() > refresh.total_seconds: + if refresh is None or age.total_seconds() > refresh.total_seconds: old_sha = run_git_command(["git", "rev-parse", "HEAD"], str(repo_dir)) _LOGGER.info("Updating %s", key) _LOGGER.debug("Location: %s", repo_dir) From 7466773ac8afab753afb520514df869e4bcb54bb Mon Sep 17 00:00:00 2001 From: tljuniper <48209000+tljuniper@users.noreply.github.com> Date: Sun, 5 Mar 2023 21:28:46 +0100 Subject: [PATCH 072/115] substitutions: Don't warn when passwords look like a substitution (#4161) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/substitutions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 5a3da1abbe..b65410cbed 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -66,7 +66,7 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: - if not ignore_missing: + if not ignore_missing and "password" not in path: _LOGGER.warning( "Found '%s' (see %s) which looks like a substitution, but '%s' was " "not declared", From b8538c2c12620f16fada80824f50f70fbf6de1d9 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 6 Mar 2023 00:02:36 +0100 Subject: [PATCH 073/115] Fix typo (#4515) --- esphome/core/macros.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/macros.h b/esphome/core/macros.h index 70ceaf58f4..ee53d20ad1 100644 --- a/esphome/core/macros.h +++ b/esphome/core/macros.h @@ -1,4 +1,4 @@ #pragma once -// Helper macro to define a version code, whos evalue can be compared against other version codes. +// Helper macro to define a version code, whose value can be compared against other version codes. #define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch)) From 29571a1acd22e08f25ced2aec80c3c40fd3ee065 Mon Sep 17 00:00:00 2001 From: Fredrik Gustafsson Date: Mon, 6 Mar 2023 19:04:35 +0100 Subject: [PATCH 074/115] implement pairing for bluetooth proxy (#4475) * default to just-works encryption This patch will turn on encryption when making active connections in order to comply with just-works BLE encryption. * Revert "default to just-works encryption" This reverts commit 05bc9e9f1cca9c41cbc7434379cbd5eb6dbb1034. * implement pair method * adhere to clang formatter * fix oopsie * bump bluetooth_proxy_version * add auth callback * generate new protos * fix another oopsie * add pairing status to connection * clear paired on connect() * lint * add unpair ("forget") ble method * compile protos * fix oopsie * add missing unpairing method * add unpairing * fix get_paired return type * remove unused memcpy Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * change to is_paired Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update bluetooth_proxy.cpp * actually add missing method * send auth cb on set_encryption failure * cleanup from havin the worst test setup * lint * match auth events to bd_addr * add second addr check to auth cb * add addr check to third auth cb --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 20 +++++ esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_pb2.cpp | 86 +++++++++++++++++++ esphome/components/api/api_pb2.h | 26 ++++++ esphome/components/api/api_pb2_service.cpp | 16 ++++ esphome/components/api/api_pb2_service.h | 6 ++ esphome/components/api/api_server.cpp | 22 +++++ esphome/components/api/api_server.h | 2 + .../bluetooth_proxy/bluetooth_connection.cpp | 19 ++++ .../bluetooth_proxy/bluetooth_connection.h | 1 + .../bluetooth_proxy/bluetooth_proxy.cpp | 29 +++++-- .../bluetooth_proxy/bluetooth_proxy.h | 9 ++ .../esp32_ble_client/ble_client_base.cpp | 8 ++ .../esp32_ble_client/ble_client_base.h | 3 + 14 files changed, 240 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index eb639f2065..1cebdd0cbe 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1339,3 +1339,23 @@ message BluetoothGATTNotifyResponse { uint64 address = 1; uint32 handle = 2; } + +message BluetoothDevicePairingResponse { + option (id) = 85; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool paired = 2; + int32 error = 3; +} + +message BluetoothDeviceUnpairingResponse { + option (id) = 86; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool success = 2; + int32 error = 3; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 487aa53193..40a5a230a5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -953,7 +953,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() ? 3 : 1; + resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() ? 4 : 1; #endif return resp; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3fc1bfa95d..381f8b3c46 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -5974,6 +5974,92 @@ void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool BluetoothDevicePairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->paired = value.as_bool(); + return true; + } + case 3: { + this->error = value.as_int32(); + return true; + } + default: + return false; + } +} +void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_bool(2, this->paired); + buffer.encode_int32(3, this->error); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothDevicePairingResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDevicePairingResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" paired: "); + out.append(YESNO(this->paired)); + out.append("\n"); + + out.append(" error: "); + sprintf(buffer, "%d", this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothDeviceUnpairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->success = value.as_bool(); + return true; + } + case 3: { + this->error = value.as_int32(); + return true; + } + default: + return false; + } +} +void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_bool(2, this->success); + buffer.encode_int32(3, this->error); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceUnpairingResponse {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + + out.append(" error: "); + sprintf(buffer, "%d", this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e192892e72..e9025142e9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1528,6 +1528,32 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothDevicePairingResponse : public ProtoMessage { + public: + uint64_t address{0}; + bool paired{false}; + int32_t error{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothDeviceUnpairingResponse : public ProtoMessage { + public: + uint64_t address{0}; + bool success{false}; + int32_t error{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index b603ade9de..7ee9e56192 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -425,6 +425,22 @@ bool APIServerConnectionBase::send_bluetooth_gatt_notify_response(const Bluetoot return this->send_message_(msg, 84); } #endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_device_pairing_response(const BluetoothDevicePairingResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_device_pairing_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 85); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_device_unpairing_response(const BluetoothDeviceUnpairingResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_device_unpairing_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 86); +} +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 3cb8b59ba5..f1879b2dba 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -209,6 +209,12 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_device_pairing_response(const BluetoothDevicePairingResponse &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_device_unpairing_response(const BluetoothDeviceUnpairingResponse &msg); #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index dbd732f466..6e28637241 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -309,6 +309,28 @@ void APIServer::send_bluetooth_device_connection(uint64_t address, bool connecte } } +void APIServer::send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error) { + BluetoothDevicePairingResponse call; + call.address = address; + call.paired = paired; + call.error = error; + + for (auto &client : this->clients_) { + client->send_bluetooth_device_pairing_response(call); + } +} + +void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error) { + BluetoothDeviceUnpairingResponse call; + call.address = address; + call.success = success; + call.error = error; + + for (auto &client : this->clients_) { + client->send_bluetooth_device_unpairing_response(call); + } +} + void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { BluetoothConnectionsFreeResponse call; call.free = free; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 8e69a77475..5f92e6b058 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -78,6 +78,8 @@ class APIServer : public Component, public Controller { #ifdef USE_BLUETOOTH_PROXY void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); + void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); + void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); void send_bluetooth_connections_free(uint8_t free, uint8_t limit); void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call); diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 09611b6174..9354ab36d6 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -158,6 +158,25 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga return true; } +void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEClientBase::gap_event_handler(event, param); + + switch (event) { + case ESP_GAP_BLE_AUTH_CMPL_EVT: + if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) + break; + if (param->ble_security.auth_cmpl.success) { + api::global_api_server->send_bluetooth_device_pairing(this->address_, true); + } else { + api::global_api_server->send_bluetooth_device_pairing(this->address_, false, + param->ble_security.auth_cmpl.fail_reason); + } + break; + default: + break; + } +} + esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { if (!this->connected()) { ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_, diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index fde074d17f..8b13f4d1c2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -13,6 +13,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { public: bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; esp_err_t read_characteristic(uint16_t handle); esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 017e1bf83f..55fabf05ef 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -257,12 +257,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str()); } if (msg.has_address_type) { - connection->remote_bda_[0] = (msg.address >> 40) & 0xFF; - connection->remote_bda_[1] = (msg.address >> 32) & 0xFF; - connection->remote_bda_[2] = (msg.address >> 24) & 0xFF; - connection->remote_bda_[3] = (msg.address >> 16) & 0xFF; - connection->remote_bda_[4] = (msg.address >> 8) & 0xFF; - connection->remote_bda_[5] = (msg.address >> 0) & 0xFF; + uint64_to_bd_addr(msg.address, connection->remote_bda_); connection->set_remote_addr_type(static_cast(msg.address_type)); connection->set_state(espbt::ClientState::DISCOVERED); } else { @@ -290,9 +285,27 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } break; } - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: { + auto *connection = this->get_connection_(msg.address, false); + if (connection != nullptr) { + if (!connection->is_paired()) { + auto err = connection->pair(); + if (err != ESP_OK) { + api::global_api_server->send_bluetooth_device_pairing(msg.address, false, err); + } + } else { + api::global_api_server->send_bluetooth_device_pairing(msg.address, true); + } + } break; + } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: { + esp_bd_addr_t address; + uint64_to_bd_addr(msg.address, address); + esp_err_t ret = esp_ble_remove_bond_device(address); + api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret); + break; + } } } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 5d3b385bec..b99e9a8527 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -44,6 +44,15 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com int get_bluetooth_connections_free(); int get_bluetooth_connections_limit() { return this->connections_.size(); } + static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { + bd_addr[0] = (address >> 40) & 0xff; + bd_addr[1] = (address >> 32) & 0xff; + bd_addr[2] = (address >> 24) & 0xff; + bd_addr[3] = (address >> 16) & 0xff; + bd_addr[4] = (address >> 8) & 0xff; + bd_addr[5] = (address >> 0) & 0xff; + } + void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2793a74c5a..9ca82c7239 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -62,6 +62,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { void BLEClientBase::connect() { ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), this->remote_addr_type_); + this->paired_ = false; auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); if (ret) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), @@ -72,6 +73,8 @@ void BLEClientBase::connect() { } } +esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } + void BLEClientBase::disconnect() { if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) return; @@ -247,11 +250,15 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ switch (event) { // This event is sent by the server when it requests security case ESP_GAP_BLE_SEC_REQ_EVT: + if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) + break; ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); break; // This event is sent once authentication has completed case ESP_GAP_BLE_AUTH_CMPL_EVT: + if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) + break; esp_bd_addr_t bd_addr; memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), @@ -260,6 +267,7 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(), param->ble_security.auth_cmpl.fail_reason); } else { + this->paired_ = true; ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 9adf239512..2879da4d8c 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -33,6 +33,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void connect() override; + esp_err_t pair(); void disconnect(); void release_services(); @@ -71,6 +72,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void set_remote_addr_type(esp_ble_addr_type_t address_type) { this->remote_addr_type_ = address_type; } uint16_t get_conn_id() const { return this->conn_id_; } uint64_t get_address() const { return this->address_; } + bool is_paired() const { return this->paired_; } uint8_t get_connection_index() const { return this->connection_index_; } @@ -86,6 +88,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint8_t connection_index_; int16_t service_count_{0}; uint16_t mtu_{23}; + bool paired_{false}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; std::vector services_; From 5a07e8d32b7466f04cc67d2d49b8f3792e9db66c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Mar 2023 17:06:42 +1300 Subject: [PATCH 075/115] Bump docker dependencies (#4526) * Bump curl to 7.74.0-1.3+deb11u7 * Bump docker base images --- docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index dc9c4b7101..59901d7b2c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,9 +6,9 @@ ARG BASEIMGTYPE=docker # https://github.com/hassio-addons/addon-debian-base/releases -FROM ghcr.io/hassio-addons/debian-base:6.2.0 AS base-hassio +FROM ghcr.io/hassio-addons/debian-base:6.2.3 AS base-hassio # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye -FROM debian:bullseye-20221024-slim AS base-docker +FROM debian:bullseye-20230208-slim AS base-docker FROM base-${BASEIMGTYPE} AS base @@ -26,7 +26,7 @@ RUN \ python3-cryptography=3.3.2-1 \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ - curl=7.74.0-1.3+deb11u5 \ + curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ && rm -rf \ /tmp/* \ From 3227ef4bca1c6585258b7a256b70b1770c6c3c64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 17:07:15 +1300 Subject: [PATCH 076/115] Bump aioesphomeapi from 13.4.0 to 13.5.0 (#4525) Bumps [aioesphomeapi](https://github.com/esphome/aioesphomeapi) from 13.4.0 to 13.5.0. - [Release notes](https://github.com/esphome/aioesphomeapi/releases) - [Commits](https://github.com/esphome/aioesphomeapi/compare/v13.4.0...v13.5.0) --- updated-dependencies: - dependency-name: aioesphomeapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bfad2433c6..b49737d991 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.6 # When updating platformio, also update Dockerfile esptool==4.5 click==8.1.3 esphome-dashboard==20230214.0 -aioesphomeapi==13.4.0 +aioesphomeapi==13.5.0 zeroconf==0.47.3 # esp-idf requires this, but doesn't bundle it by default From 3773c385c702ba64d6a9836822334db553b6c016 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 7 Mar 2023 05:16:42 +0100 Subject: [PATCH 077/115] Ensure component is ready before update. (#4523) Co-authored-by: Your Name --- esphome/core/base_automation.h | 2 +- esphome/core/component.cpp | 4 ++++ esphome/core/component.h | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 5a4fb65198..daa09b912e 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -317,7 +317,7 @@ template class UpdateComponentAction : public Action { UpdateComponentAction(PollingComponent *component) : component_(component) {} void play(Ts... x) override { - if (this->component_->is_failed()) + if (!this->component_->is_ready()) return; this->component_->update(); } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b1ace8b2d1..49ef8ecde7 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -135,6 +135,10 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } +bool Component::is_ready() { + return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || + (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; +} bool Component::can_proceed() { return true; } bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; } diff --git a/esphome/core/component.h b/esphome/core/component.h index 769e74e645..7382f1c617 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -119,6 +119,8 @@ class Component { bool is_failed(); + bool is_ready(); + virtual bool can_proceed(); bool status_has_warning(); From 05ab49a6152cb46a877dcbf851b1127c764725a8 Mon Sep 17 00:00:00 2001 From: Nathaniel Wesley Filardo Date: Tue, 7 Mar 2023 04:19:49 +0000 Subject: [PATCH 078/115] climate: add on_control callbacks (#4511) This lets downstream components respond to climate configuration changes, which take place through ClimateCall objects, without also being notified every time the state changes, which happens every time the input sensor announces a new value. FIXES https://github.com/esphome/feature-requests/issues/2136 --- esphome/components/climate/__init__.py | 7 +++++++ esphome/components/climate/automation.h | 7 +++++++ esphome/components/climate/climate.cpp | 5 +++++ esphome/components/climate/climate.h | 9 +++++++++ esphome/const.py | 1 + tests/test1.yaml | 2 ++ 6 files changed, 31 insertions(+) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 709d0d12ed..4a16c3fb7d 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( CONF_MODE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_ON_CONTROL, CONF_ON_STATE, CONF_PRESET, CONF_PRESET_COMMAND_TOPIC, @@ -127,6 +128,7 @@ def single_visual_temperature(value): # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) +ControlTrigger = climate_ns.class_("ControlTrigger", automation.Trigger.template()) VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any( single_visual_temperature, @@ -203,6 +205,11 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_ON_CONTROL): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), + } + ), cv.Optional(CONF_ON_STATE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 3145358dab..9b06563eb4 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -42,6 +42,13 @@ template class ControlAction : public Action { Climate *climate_; }; +class ControlTrigger : public Trigger<> { + public: + ControlTrigger(Climate *climate) { + climate->add_on_control_callback([this]() { this->trigger(); }); + } +}; + class StateTrigger : public Trigger<> { public: StateTrigger(Climate *climate) { diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index b80fe640c8..37572ae913 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -44,6 +44,7 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } + this->parent_->control_callback_.call(); this->parent_->control(*this); } void ClimateCall::validate_() { @@ -317,6 +318,10 @@ void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +void Climate::add_on_control_callback(std::function &&callback) { + this->control_callback_.add(std::move(callback)); +} + // Random 32bit value; If this changes existing restore preferences are invalidated static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 8cc260abbe..520036f718 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -219,6 +219,14 @@ class Climate : public EntityBase { */ void add_on_state_callback(std::function &&callback); + /** + * Add a callback for the climate device configuration; each time the configuration parameters of a climate device + * is updated (using perform() of a ClimateCall), this callback will be called, before any on_state callback. + * + * @param callback The callback to call. + */ + void add_on_control_callback(std::function &&callback); + /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description * for more info. * @return A new ClimateCall instance targeting this climate device. @@ -285,6 +293,7 @@ class Climate : public EntityBase { void dump_traits_(const char *tag); CallbackManager state_callback_{}; + CallbackManager control_callback_{}; ESPPreferenceObject rtc_; optional visual_min_temperature_override_{}; optional visual_max_temperature_override_{}; diff --git a/esphome/const.py b/esphome/const.py index d821a76b75..0ab9dd54bf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -447,6 +447,7 @@ CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" CONF_ON_CONNECT = "on_connect" +CONF_ON_CONTROL = "on_control" CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" diff --git a/tests/test1.yaml b/tests/test1.yaml index d43490c8cc..a554d45771 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2052,6 +2052,8 @@ climate: name: Midea IR use_fahrenheit: true - platform: midea + on_control: + logger.log: Control message received! on_state: logger.log: State changed! id: midea_unit From 6ecf4ecac6125219253672e734d7f7f14bec9b11 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Mon, 6 Mar 2023 23:25:14 -0500 Subject: [PATCH 079/115] FS3000 sensor (#4502) * Add support for FS3000 sensor. * add fs3000 to test yaml * Clean up code with clang. * Clean up sensor.py file. * Update CODEOWNERS file. * Apply suggestions from code review regarding sensor.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Apply suggestions from code review for basic issues regarding C++ code - removed unnecessary default for FS3000Model - use "this->" before any sensor update Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Move model setup to overall setup function. * Remove unneeded CONF_ID from sensor.py * Run clang-format * Move set_model code to header file now that it is simplified * Update fs3000.h --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/fs3000/__init__.py | 0 esphome/components/fs3000/fs3000.cpp | 107 ++++++++++++++++++++++++++ esphome/components/fs3000/fs3000.h | 35 +++++++++ esphome/components/fs3000/sensor.py | 50 ++++++++++++ tests/test1.yaml | 6 ++ 6 files changed, 199 insertions(+) create mode 100644 esphome/components/fs3000/__init__.py create mode 100644 esphome/components/fs3000/fs3000.cpp create mode 100644 esphome/components/fs3000/fs3000.h create mode 100644 esphome/components/fs3000/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4e3c6c3510..e8f264b633 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -90,6 +90,7 @@ esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh +esphome/components/fs3000/* @kahrendt esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle diff --git a/esphome/components/fs3000/__init__.py b/esphome/components/fs3000/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/fs3000/fs3000.cpp b/esphome/components/fs3000/fs3000.cpp new file mode 100644 index 0000000000..fb729ed0a0 --- /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 0000000000..be3680e7e1 --- /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 0000000000..0c50f52979 --- /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/tests/test1.yaml b/tests/test1.yaml index a554d45771..4312f2c613 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1224,6 +1224,12 @@ sensor: - platform: sen21231 name: "Person Sensor" i2c_id: i2c_bus + - platform: fs3000 + name: "Air Velocity" + model: 1005 + update_interval: 60s + i2c_id: i2c_bus + esp32_touch: setup_mode: false iir_filter: 10ms From bb5ab8b36d2664ebb01bebe5331bb3e19e60166b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 19:38:05 +1300 Subject: [PATCH 080/115] Bump esptool from 4.5 to 4.5.1 (#4497) Bumps [esptool](https://github.com/espressif/esptool) from 4.5 to 4.5.1. - [Release notes](https://github.com/espressif/esptool/releases) - [Commits](https://github.com/espressif/esptool/compare/v4.5...v4.5.1) --- updated-dependencies: - dependency-name: esptool dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b49737d991..df041db26f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.6 # When updating platformio, also update Dockerfile -esptool==4.5 +esptool==4.5.1 click==8.1.3 esphome-dashboard==20230214.0 aioesphomeapi==13.5.0 From 356efdb92c6f61de86f4bbb8204cfae8b51fbab7 Mon Sep 17 00:00:00 2001 From: bisbastuner <104585618+bisbastuner@users.noreply.github.com> Date: Tue, 7 Mar 2023 08:15:40 +0100 Subject: [PATCH 081/115] Add support for multiple devices in bme680_bsec (#3550) * Add initial support for multiple devices Re-introduce support for multiple I2C devices (it was suppressed in https://github.com/trvrnrth/esphome-bsec-bme680/commit/df37a7635f6acc3f7520f257d2645eda10b863a5). Devices are identified by their I2C address, and the BME680 can only have the 0x76 or 0x77 address, so this adds support for a maximum of two devices. * Reintegrate commit ebf13a0b Reintegrate commit https://github.com/esphome/esphome/commit/ebf13a0ba0a48453c2cc80bfc942d6a588859233 which was lost in my changes (I were working on old files) * wrong commit * wrong commit * Reintegrate commit ebf13a0b Reintegrate commit https://github.com/esphome/esphome/commit/ebf13a0ba0a48453c2cc80bfc942d6a588859233 which was lost due to me working on old files * Reintroduce newlines at end of files * Reintroduce newlines at end of files * Adhere to codebase standards Obey the "All uses of class members and member functions should be prefixed with this-> to distinguish them from global functions in code review" rule of the Codebase Standards * Fix formatting according to clang-format * Perform the BSEC library reinitialization+snapshot only when more than one device is present * Fix formatting according to clang-format * Degrade abort message in restore_state_() from warning to verbose This always happen at initial setup, so it's not a really useful message; when some real problems arise, we'll have a more useful warning from snapshot_state_() Co-authored-by: Trevor North * Reduce peak stack usage to avoid bootloops on ESP8266 Achieved mainly by moving the work_buffer needed by the BSEC library to the heap, as a single global work buffer shared by all instances. ::set_config_ has been reverted to a code path similar to the original, as that reduces peak stack usage enough to be OK on ESP8266 even without moving the work_buffer to the heap. * Fix formatting according to clang-format * Add support for devices with the same i2c address Devices are now identified using their index in the BME680BSECComponent::instances member, which became a vector. This allows adding two devices with the same i2c address (which should be placed on different i2c buses). Since a BME680 can only have an address of 0x76 or 0x77, a maximum of 2 devices could be added before this commit. Now there is no theoretical limit on the number of devices which could be added. * Fix formatting according to clang-format * Fix formatting according to clang-format --------- Co-authored-by: Trevor North Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/bme680_bsec/__init__.py | 2 + .../components/bme680_bsec/bme680_bsec.cpp | 272 ++++++++++++------ esphome/components/bme680_bsec/bme680_bsec.h | 29 +- 3 files changed, 215 insertions(+), 88 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 83e519f8aa..c9813c4974 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensor", "text_sensor"] +MULTI_CONF = True CONF_BME680_BSEC_ID = "bme680_bsec_id" CONF_TEMPERATURE_OFFSET = "temperature_offset" @@ -54,6 +55,7 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + cg.add(var.set_device_id(str(config[CONF_ID]))) cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index b84ca3318b..2b1b0dc948 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -10,19 +10,24 @@ static const char *const TAG = "bme680_bsec.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; -BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +std::vector + BME680BSECComponent::instances; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +uint8_t BME680BSECComponent::work_buffer_[BSEC_MAX_WORKBUFFER_SIZE] = {0}; void BME680BSECComponent::setup() { - ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); - BME680BSECComponent::instance = this; + ESP_LOGCONFIG(TAG, "Setting up BME680(%s) via BSEC...", this->device_id_.c_str()); - this->bsec_status_ = bsec_init(); - if (this->bsec_status_ != BSEC_OK) { - this->mark_failed(); - return; - } + uint8_t new_idx = BME680BSECComponent::instances.size(); + BME680BSECComponent::instances.push_back(this); - this->bme680_.dev_id = this->address_; + this->bsec_state_data_valid_ = false; + + // Initialize the bme680_ structure (passed-in to the bme680_* functions) and the BME680 device + this->bme680_.dev_id = + new_idx; // This is a "Place holder to store the id of the device structure" (see bme680_defs.h). + // This will be passed-in as first parameter to the next "read" and "write" function pointers. + // We currently use the index of the object in the BME680BSECComponent::instances vector to identify + // the different devices in the system. this->bme680_.intf = BME680_I2C_INTF; this->bme680_.read = BME680BSECComponent::read_bytes_wrapper; this->bme680_.write = BME680BSECComponent::write_bytes_wrapper; @@ -35,29 +40,30 @@ void BME680BSECComponent::setup() { return; } - if (this->sample_rate_ == SAMPLE_RATE_ULP) { - const uint8_t bsec_config[] = { -#include "config/generic_33v_300s_28d/bsec_iaq.txt" - }; - this->set_config_(bsec_config); - } else { - const uint8_t bsec_config[] = { -#include "config/generic_33v_3s_28d/bsec_iaq.txt" - }; - this->set_config_(bsec_config); - } - this->update_subscription_(); - if (this->bsec_status_ != BSEC_OK) { + // Initialize the BSEC library + if (this->reinit_bsec_lib_() != 0) { this->mark_failed(); return; } + // Load the BSEC library state from storage this->load_state_(); } -void BME680BSECComponent::set_config_(const uint8_t *config) { - uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; - this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer)); +void BME680BSECComponent::set_config_() { + if (this->sample_rate_ == SAMPLE_RATE_ULP) { + const uint8_t config[] = { +#include "config/generic_33v_300s_28d/bsec_iaq.txt" + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } else { + const uint8_t config[] = { +#include "config/generic_33v_3s_28d/bsec_iaq.txt" + }; + this->bsec_status_ = + bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + } } float BME680BSECComponent::calc_sensor_sample_rate_(SampleRate sample_rate) { @@ -118,10 +124,12 @@ void BME680BSECComponent::update_subscription_() { uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; this->bsec_status_ = bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); + ESP_LOGV(TAG, "%s: updating subscription for %d virtual sensors (out=%d sensors)", this->device_id_.c_str(), + num_virtual_sensors, num_sensor_settings); } void BME680BSECComponent::dump_config() { - ESP_LOGCONFIG(TAG, "BME680 via BSEC:"); + ESP_LOGCONFIG(TAG, "%s via BSEC:", this->device_id_.c_str()); bsec_version_t version; bsec_get_version(&version); @@ -185,23 +193,31 @@ void BME680BSECComponent::run_() { return; } - ESP_LOGV(TAG, "Performing sensor run"); + ESP_LOGV(TAG, "%s: Performing sensor run", this->device_id_.c_str()); - bsec_bme_settings_t bme680_settings; - this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings); + // Restore BSEC library state + // The reinit_bsec_lib_ method is computationally expensive: it takes 1200÷2900 microseconds on a ESP32. + // It can be skipped entirely when there is only one device (since the BSEC library won't be shared) + if (BME680BSECComponent::instances.size() > 1) { + int res = this->reinit_bsec_lib_(); + if (res != 0) + return; + } + + this->bsec_status_ = bsec_sensor_control(curr_time_ns, &this->bme680_settings_); if (this->bsec_status_ < BSEC_OK) { ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); return; } - this->next_call_ns_ = bme680_settings.next_call; + this->next_call_ns_ = this->bme680_settings_.next_call; - if (bme680_settings.trigger_measurement) { - this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling; - this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling; - this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling; - this->bme680_.gas_sett.run_gas = bme680_settings.run_gas; - this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature; - this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration; + if (this->bme680_settings_.trigger_measurement) { + this->bme680_.tph_sett.os_temp = this->bme680_settings_.temperature_oversampling; + this->bme680_.tph_sett.os_pres = this->bme680_settings_.pressure_oversampling; + this->bme680_.tph_sett.os_hum = this->bme680_settings_.humidity_oversampling; + this->bme680_.gas_sett.run_gas = this->bme680_settings_.run_gas; + this->bme680_.gas_sett.heatr_temp = this->bme680_settings_.heater_temperature; + this->bme680_.gas_sett.heatr_dur = this->bme680_settings_.heating_duration; this->bme680_.power_mode = BME680_FORCED_MODE; uint16_t desired_settings = BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_GAS_SENSOR_SEL; this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_); @@ -218,19 +234,26 @@ void BME680BSECComponent::run_() { uint16_t meas_dur = 0; bme680_get_profile_dur(&meas_dur, &this->bme680_); + + // Since we are about to go "out of scope" in the loop, take a snapshot of the state now so we can restore it later + // TODO: it would be interesting to see if this is really needed here, or if it's needed only after each + // bsec_do_steps() call + if (BME680BSECComponent::instances.size() > 1) + this->snapshot_state_(); + ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); - this->set_timeout("read", meas_dur, - [this, curr_time_ns, bme680_settings]() { this->read_(curr_time_ns, bme680_settings); }); + this->set_timeout("read", meas_dur, [this]() { this->read_(); }); } else { ESP_LOGV(TAG, "Measurement not required"); - this->read_(curr_time_ns, bme680_settings); + this->read_(); } } -void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings) { - ESP_LOGV(TAG, "Reading data"); +void BME680BSECComponent::read_() { + ESP_LOGV(TAG, "%s: Reading data", this->device_id_.c_str()); + int64_t curr_time_ns = this->get_time_ns_(); - if (bme680_settings.trigger_measurement) { + if (this->bme680_settings_.trigger_measurement) { while (this->bme680_.power_mode != BME680_SLEEP_MODE) { this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); if (this->bme680_status_ != BME680_OK) { @@ -239,7 +262,7 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme } } - if (!bme680_settings.process_data) { + if (!this->bme680_settings_.process_data) { ESP_LOGV(TAG, "Data processing not required"); return; } @@ -259,35 +282,35 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance uint8_t num_inputs = 0; - if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) { + if (this->bme680_settings_.process_data & BSEC_PROCESS_TEMPERATURE) { inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; inputs[num_inputs].signal = data.temperature / 100.0f; - inputs[num_inputs].time_stamp = trigger_time_ns; + inputs[num_inputs].time_stamp = curr_time_ns; num_inputs++; // Temperature offset from the real temperature due to external heat sources inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; inputs[num_inputs].signal = this->temperature_offset_; - inputs[num_inputs].time_stamp = trigger_time_ns; + inputs[num_inputs].time_stamp = curr_time_ns; num_inputs++; } - if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) { + if (this->bme680_settings_.process_data & BSEC_PROCESS_HUMIDITY) { inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; inputs[num_inputs].signal = data.humidity / 1000.0f; - inputs[num_inputs].time_stamp = trigger_time_ns; + inputs[num_inputs].time_stamp = curr_time_ns; num_inputs++; } - if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) { + if (this->bme680_settings_.process_data & BSEC_PROCESS_PRESSURE) { inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; inputs[num_inputs].signal = data.pressure; - inputs[num_inputs].time_stamp = trigger_time_ns; + inputs[num_inputs].time_stamp = curr_time_ns; num_inputs++; } - if (bme680_settings.process_data & BSEC_PROCESS_GAS) { + if (this->bme680_settings_.process_data & BSEC_PROCESS_GAS) { if (data.status & BME680_GASM_VALID_MSK) { inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; inputs[num_inputs].signal = data.gas_resistance; - inputs[num_inputs].time_stamp = trigger_time_ns; + inputs[num_inputs].time_stamp = curr_time_ns; num_inputs++; } else { ESP_LOGD(TAG, "BME680 did not report gas data"); @@ -298,6 +321,22 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme return; } + // Restore BSEC library state + // The reinit_bsec_lib_ method is computationally expensive: it takes 1200÷2900 microseconds on a ESP32. + // It can be skipped entirely when there is only one device (since the BSEC library won't be shared) + if (BME680BSECComponent::instances.size() > 1) { + int res = this->reinit_bsec_lib_(); + if (res != 0) + return; + // Now that the BSEC library has been re-initialized, bsec_sensor_control *NEEDS* to be called in order to support + // multiple devices with a different set of enabled sensors (even if the bme680_settings_ data is not used) + this->bsec_status_ = bsec_sensor_control(curr_time_ns, &this->bme680_settings_); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); + return; + } + } + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs); @@ -305,6 +344,13 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_); return; } + ESP_LOGV(TAG, "%s: after bsec_do_steps: num_inputs=%d num_outputs=%d", this->device_id_.c_str(), num_inputs, + num_outputs); + + // Since we are about to go "out of scope" in the loop, take a snapshot of the state now so we can restore it later + if (BME680BSECComponent::instances.size() > 1) + this->snapshot_state_(); + if (num_outputs < 1) { ESP_LOGD(TAG, "No signal outputs provided by BSEC"); return; @@ -314,7 +360,7 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme } void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { - ESP_LOGV(TAG, "Queuing sensor state publish actions"); + ESP_LOGV(TAG, "%s: Queuing sensor state publish actions", this->device_id_.c_str()); for (uint8_t i = 0; i < num_outputs; i++) { float signal = outputs[i].signal; switch (outputs[i].sensor_id) { @@ -376,12 +422,20 @@ void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const sensor->publish_state(value); } -int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { - return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1; +// Communication function - read +// First parameter is the "dev_id" member of our "bme680_" object, which is passed-back here as-is +int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t devid, uint8_t a_register, uint8_t *data, uint16_t len) { + BME680BSECComponent *inst = instances[devid]; + // Use the I2CDevice::read_bytes method to perform the actual I2C register read + return inst->read_bytes(a_register, data, len) ? 0 : -1; } -int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { - return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1; +// Communication function - write +// First parameter is the "dev_id" member of our "bme680_" object, which is passed-back here as-is +int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_register, uint8_t *data, uint16_t len) { + BME680BSECComponent *inst = instances[devid]; + // Use the I2CDevice::write_bytes method to perform the actual I2C register write + return inst->write_bytes(a_register, data, len) ? 0 : -1; } void BME680BSECComponent::delay_ms(uint32_t period) { @@ -389,41 +443,97 @@ void BME680BSECComponent::delay_ms(uint32_t period) { delay(period); } +// Fetch the BSEC library state and save it in the bsec_state_data_ member (volatile memory) +// Used to share the library when using more than one sensor +void BME680BSECComponent::snapshot_state_() { + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + this->bsec_status_ = bsec_get_state(0, this->bsec_state_data_, BSEC_MAX_STATE_BLOB_SIZE, this->work_buffer_, + sizeof(this->work_buffer_), &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "%s: Failed to fetch BSEC library state for snapshot (BSEC Error Code %d)", this->device_id_.c_str(), + this->bsec_status_); + return; + } + this->bsec_state_data_valid_ = true; +} + +// Restores the BSEC library state from a snapshot in memory +// Used to share the library when using more than one sensor +void BME680BSECComponent::restore_state_() { + if (!this->bsec_state_data_valid_) { + ESP_LOGV(TAG, "%s: BSEC state data NOT valid, aborting restore_state_()", this->device_id_.c_str()); + return; + } + + this->bsec_status_ = + bsec_set_state(this->bsec_state_data_, BSEC_MAX_STATE_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to restore BSEC library state (BSEC Error Code %d)", this->bsec_status_); + return; + } +} + +int BME680BSECComponent::reinit_bsec_lib_() { + this->bsec_status_ = bsec_init(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return -1; + } + + this->set_config_(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return -2; + } + + this->restore_state_(); + + this->update_subscription_(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return -3; + } + + return 0; +} + void BME680BSECComponent::load_state_() { - uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_)); + uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_); this->bsec_state_ = global_preferences->make_preference(hash, true); - uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; - if (this->bsec_state_.load(&state)) { - ESP_LOGV(TAG, "Loading state"); - uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; - this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); - if (this->bsec_status_ != BSEC_OK) { - ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_); - } - ESP_LOGI(TAG, "Loaded state"); + if (!this->bsec_state_.load(&this->bsec_state_data_)) { + // No saved BSEC library state available + return; } + + ESP_LOGV(TAG, "%s: Loading BSEC library state", this->device_id_.c_str()); + this->bsec_status_ = + bsec_set_state(this->bsec_state_data_, BSEC_MAX_STATE_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "%s: Failed to load BSEC library state (BSEC Error Code %d)", this->device_id_.c_str(), + this->bsec_status_); + return; + } + // All OK: set the BSEC state data as valid + this->bsec_state_data_valid_ = true; + ESP_LOGI(TAG, "%s: Loaded BSEC library state", this->device_id_.c_str()); } void BME680BSECComponent::save_state_(uint8_t accuracy) { if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { return; } - - ESP_LOGV(TAG, "Saving state"); - - uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; - uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; - uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; - - this->bsec_status_ = - bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); - if (this->bsec_status_ != BSEC_OK) { - ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_); - return; + if (BME680BSECComponent::instances.size() <= 1) { + // When a single device is in use, no snapshot is taken regularly so one is taken now + // On multiple devices, a snapshot is taken at every loop, so there is no need to take one here + this->snapshot_state_(); } + if (!this->bsec_state_data_valid_) + return; - if (!this->bsec_state_.save(&state)) { + ESP_LOGV(TAG, "%s: Saving state", this->device_id_.c_str()); + + if (!this->bsec_state_.save(&this->bsec_state_data_)) { ESP_LOGW(TAG, "Failed to save state"); return; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 6fe8f8fef7..a97ad2f53e 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -31,6 +31,7 @@ enum SampleRate { class BME680BSECComponent : public Component, public i2c::I2CDevice { public: + void set_device_id(const std::string &devid) { this->device_id_.assign(devid); } void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } @@ -50,9 +51,9 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; } void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; } - static BME680BSECComponent *instance; - static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); - static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static std::vector instances; + static int8_t read_bytes_wrapper(uint8_t devid, uint8_t a_register, uint8_t *data, uint16_t len); + static int8_t write_bytes_wrapper(uint8_t devid, uint8_t a_register, uint8_t *data, uint16_t len); static void delay_ms(uint32_t period); void setup() override; @@ -61,23 +62,33 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void loop() override; protected: - void set_config_(const uint8_t *config); + void set_config_(); float calc_sensor_sample_rate_(SampleRate sample_rate); void update_subscription_(); void run_(); - void read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings); + void read_(); void publish_(const bsec_output_t *outputs, uint8_t num_outputs); int64_t get_time_ns_(); void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); - void load_state_(); - void save_state_(uint8_t accuracy); + void snapshot_state_(); // Fetch the current BSEC library state and save it in the bsec_state_data_ member (volatile + // memory) + void restore_state_(); // Push the state contained in the bsec_state_data_ member (volatile memory) to the BSEC + // library + int reinit_bsec_lib_(); // Prepare the BSEC library to be used again after this object returns active + // (as the library may have been used by other objects) + void load_state_(); // Initialize the ESP preferences object; retrieve the BSEC library state from the ESP + // preferences (storage); then save it in the bsec_state_data_ member (volatile memory) and + // push it to the BSEC library + void save_state_( + uint8_t accuracy); // Save the bsec_state_data_ member (volatile memory) to the ESP preferences (storage) void queue_push_(std::function &&f) { this->queue_.push(std::move(f)); } + static uint8_t work_buffer_[BSEC_MAX_WORKBUFFER_SIZE]; struct bme680_dev bme680_; bsec_library_return_t bsec_status_{BSEC_OK}; int8_t bme680_status_{BME680_OK}; @@ -88,10 +99,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { std::queue> queue_; + bool bsec_state_data_valid_; + uint8_t bsec_state_data_[BSEC_MAX_STATE_BLOB_SIZE]; // This is the current snapshot of the BSEC library state ESPPreferenceObject bsec_state_; uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day uint32_t last_state_save_ms_ = 0; + bsec_bme_settings_t bme680_settings_; + std::string device_id_; float temperature_offset_{0}; IAQMode iaq_mode_{IAQ_MODE_STATIC}; From 06ca5354b2fe2072d4cee30183f0e9dd9f59ecae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 07:21:23 +0000 Subject: [PATCH 082/115] Bump pytest from 7.2.1 to 7.2.2 (#4505) Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.1...7.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1e63f79b5e..01de8cad14 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.2.1 +pytest==7.2.2 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-asyncio==0.20.3 From 1b328da26581a201c9aaaef7be9c7910797ee2db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 07:21:45 +0000 Subject: [PATCH 083/115] Bump pylint from 2.16.2 to 2.16.4 (#4524) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.16.2 to 2.16.4. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.16.2...v2.16.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 01de8cad14..771086421e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.16.2 +pylint==2.16.4 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating black==23.1.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating From b29cc581444154e074ae80d956e657ef978fb63c Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Tue, 7 Mar 2023 13:47:25 -0700 Subject: [PATCH 084/115] Add absolute humidity component (#4519) * Import Absolute Humidity component https://PigLab.ReaperLegion.net/home-automation/hass/esphome/custom-components/absolute-humidity * Fix terminology, add some docstrings * Switch from double to float https://github.com/esphome/esphome/pull/4519#pullrequestreview-1327615169 The additional precision doesn't matter in practice. * Address code review suggestions * Lint code --- CODEOWNERS | 1 + .../components/absolute_humidity/__init__.py | 1 + .../absolute_humidity/absolute_humidity.cpp | 182 ++++++++++++++++++ .../absolute_humidity/absolute_humidity.h | 76 ++++++++ .../components/absolute_humidity/sensor.py | 56 ++++++ esphome/const.py | 3 + tests/test1.yaml | 4 + 7 files changed, 323 insertions(+) create mode 100644 esphome/components/absolute_humidity/__init__.py create mode 100644 esphome/components/absolute_humidity/absolute_humidity.cpp create mode 100644 esphome/components/absolute_humidity/absolute_humidity.h create mode 100644 esphome/components/absolute_humidity/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index e8f264b633..1ff9661add 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -11,6 +11,7 @@ esphome/*.py @esphome/core esphome/core/* @esphome/core # Integrations +esphome/components/absolute_humidity/* @DAVe3283 esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/adc128s102/* @DeerMaximum diff --git a/esphome/components/absolute_humidity/__init__.py b/esphome/components/absolute_humidity/__init__.py new file mode 100644 index 0000000000..8f113b48f6 --- /dev/null +++ b/esphome/components/absolute_humidity/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@DAVe3283"] diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp new file mode 100644 index 0000000000..13f30c996f --- /dev/null +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -0,0 +1,182 @@ +#include "esphome/core/log.h" +#include "absolute_humidity.h" + +namespace esphome { +namespace absolute_humidity { + +static const char *const TAG = "absolute_humidity.sensor"; + +void AbsoluteHumidityComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up absolute humidity '%s'...", this->get_name().c_str()); + + ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); + this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); + if (this->temperature_sensor_->has_state()) { + this->temperature_callback_(this->temperature_sensor_->get_state()); + } + + ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); + this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); }); + if (this->humidity_sensor_->has_state()) { + this->humidity_callback_(this->humidity_sensor_->get_state()); + } +} + +void AbsoluteHumidityComponent::dump_config() { + LOG_SENSOR("", "Absolute Humidity", this); + + switch (this->equation_) { + case BUCK: + ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Buck"); + break; + case TETENS: + ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Tetens"); + break; + case WOBUS: + ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Wobus"); + break; + default: + ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); + break; + } + + ESP_LOGCONFIG(TAG, "Sources"); + ESP_LOGCONFIG(TAG, " Temperature: '%s'", this->temperature_sensor_->get_name().c_str()); + ESP_LOGCONFIG(TAG, " Relative Humidity: '%s'", this->humidity_sensor_->get_name().c_str()); +} + +float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; } + +void AbsoluteHumidityComponent::loop() { + if (!this->next_update_) { + return; + } + this->next_update_ = false; + + // Ensure we have source data + const bool no_temperature = std::isnan(this->temperature_); + const bool no_humidity = std::isnan(this->humidity_); + if (no_temperature || no_humidity) { + if (no_temperature) { + ESP_LOGW(TAG, "No valid state from temperature sensor!"); + } + if (no_humidity) { + ESP_LOGW(TAG, "No valid state from temperature sensor!"); + } + ESP_LOGW(TAG, "Unable to calculate absolute humidity."); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + + // Convert to desired units + const float temperature_c = this->temperature_; + const float temperature_k = temperature_c + 273.15; + const float hr = this->humidity_ / 100; + + // Calculate saturation vapor pressure + float es; + switch (this->equation_) { + case BUCK: + es = es_buck(temperature_c); + break; + case TETENS: + es = es_tetens(temperature_c); + break; + case WOBUS: + es = es_wobus(temperature_c); + break; + default: + ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); + this->publish_state(NAN); + this->status_set_error(); + return; + } + ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); + + // Calculate absolute humidity + const float absolute_humidity = vapor_density(es, hr, temperature_k); + + // Publish absolute humidity + ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity); + this->status_clear_warning(); + this->publish_state(absolute_humidity); +} + +// Buck equation (https://en.wikipedia.org/wiki/Arden_Buck_equation) +// More accurate than Tetens in normal meteorologic conditions +float AbsoluteHumidityComponent::es_buck(float temperature_c) { + float a, b, c, d; + if (temperature_c >= 0) { + a = 0.61121; + b = 18.678; + c = 234.5; + d = 257.14; + } else { + a = 0.61115; + b = 18.678; + c = 233.7; + d = 279.82; + } + return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c))); +} + +// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation) +float AbsoluteHumidityComponent::es_tetens(float temperature_c) { + float a, b; + if (temperature_c >= 0) { + a = 17.27; + b = 237.3; + } else { + a = 21.875; + b = 265.5; + } + return 0.61078 * expf((a * temperature_c) / (temperature_c + b)); +} + +// Wobus equation +// https://wahiduddin.net/calc/density_altitude.htm +// https://wahiduddin.net/calc/density_algorithms.htm +// Calculate the saturation vapor pressure (kPa) +float AbsoluteHumidityComponent::es_wobus(float t) { + // THIS FUNCTION RETURNS THE SATURATION VAPOR PRESSURE ESW (MILLIBARS) + // OVER LIQUID WATER GIVEN THE TEMPERATURE T (CELSIUS). THE POLYNOMIAL + // APPROXIMATION BELOW IS DUE TO HERMAN WOBUS, A MATHEMATICIAN WHO + // WORKED AT THE NAVY WEATHER RESEARCH FACILITY, NORFOLK, VIRGINIA, + // BUT WHO IS NOW RETIRED. THE COEFFICIENTS OF THE POLYNOMIAL WERE + // CHOSEN TO FIT THE VALUES IN TABLE 94 ON PP. 351-353 OF THE SMITH- + // SONIAN METEOROLOGICAL TABLES BY ROLAND LIST (6TH EDITION). THE + // APPROXIMATION IS VALID FOR -50 < T < 100C. + // + // Baker, Schlatter 17-MAY-1982 Original version. + + const float c0 = +0.99999683e00; + const float c1 = -0.90826951e-02; + const float c2 = +0.78736169e-04; + const float c3 = -0.61117958e-06; + const float c4 = +0.43884187e-08; + const float c5 = -0.29883885e-10; + const float c6 = +0.21874425e-12; + const float c7 = -0.17892321e-14; + const float c8 = +0.11112018e-16; + const float c9 = -0.30994571e-19; + const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9))))))))); + return 0.61078 / pow(p, 8); +} + +// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ +// H/T to https://esphome.io/cookbook/bme280_environment.html +// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ +float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { + // es = saturated vapor pressure (kPa) + // hr = relative humidity [0-1] + // ta = absolute temperature (K) + + const float ea = hr * es * 1000; // vapor pressure of the air (Pa) + const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹) + const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹) + return (ea * mw) / (r * ta); +} + +} // namespace absolute_humidity +} // namespace esphome diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h new file mode 100644 index 0000000000..9f3b9eab8b --- /dev/null +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace absolute_humidity { + +/// Enum listing all implemented saturation vapor pressure equations. +enum SaturationVaporPressureEquation { + BUCK, + TETENS, + WOBUS, +}; + +/// This class implements calculation of absolute humidity from temperature and relative humidity. +class AbsoluteHumidityComponent : public sensor::Sensor, public Component { + public: + AbsoluteHumidityComponent() = default; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void temperature_callback_(float state) { + this->next_update_ = true; + this->temperature_ = state; + } + void humidity_callback_(float state) { + this->next_update_ = true; + this->humidity_ = state; + } + + /** Buck equation for saturation vapor pressure in kPa. + * + * @param temperature_c Air temperature in °C. + */ + static float es_buck(float temperature_c); + /** Tetens equation for saturation vapor pressure in kPa. + * + * @param temperature_c Air temperature in °C. + */ + static float es_tetens(float temperature_c); + /** Wobus equation for saturation vapor pressure in kPa. + * + * @param temperature_c Air temperature in °C. + */ + static float es_wobus(float temperature_c); + + /** Calculate vapor density (absolute humidity) in g/m³. + * + * @param es Saturation vapor pressure in kPa. + * @param hr Relative humidity 0 to 1. + * @param ta Absolute temperature in K. + * @param heater_duration The duration in ms that the heater should turn on for when measuring. + */ + static float vapor_density(float es, float hr, float ta); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + bool next_update_{false}; + + float temperature_{NAN}; + float humidity_{NAN}; + SaturationVaporPressureEquation equation_; +}; + +} // namespace absolute_humidity +} // namespace esphome diff --git a/esphome/components/absolute_humidity/sensor.py b/esphome/components/absolute_humidity/sensor.py new file mode 100644 index 0000000000..f2b075f4d9 --- /dev/null +++ b/esphome/components/absolute_humidity/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + CONF_EQUATION, + ICON_WATER, + UNIT_GRAMS_PER_CUBIC_METER, +) + +absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity") +AbsoluteHumidityComponent = absolute_humidity_ns.class_( + "AbsoluteHumidityComponent", sensor.Sensor, cg.Component +) + +SaturationVaporPressureEquation = absolute_humidity_ns.enum( + "SaturationVaporPressureEquation" +) +EQUATION = { + "BUCK": SaturationVaporPressureEquation.BUCK, + "TETENS": SaturationVaporPressureEquation.TETENS, + "WOBUS": SaturationVaporPressureEquation.WOBUS, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER, + icon=ICON_WATER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(AbsoluteHumidityComponent), + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + cv.Optional(CONF_EQUATION, default="WOBUS"): cv.enum(EQUATION, upper=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(temperature_sensor)) + + humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(humidity_sensor)) + + cg.add(var.set_equation(config[CONF_EQUATION])) diff --git a/esphome/const.py b/esphome/const.py index 0ab9dd54bf..289a59b424 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -214,6 +214,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" @@ -860,6 +861,7 @@ ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal" ICON_THERMOMETER = "mdi:thermometer" ICON_TIMELAPSE = "mdi:timelapse" ICON_TIMER = "mdi:timer-outline" +ICON_WATER = "mdi:water" ICON_WATER_PERCENT = "mdi:water-percent" ICON_WEATHER_SUNSET = "mdi:weather-sunset" ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down" @@ -881,6 +883,7 @@ UNIT_DEGREE_PER_SECOND = "°/s" UNIT_DEGREES = "°" UNIT_EMPTY = "" UNIT_G = "G" +UNIT_GRAMS_PER_CUBIC_METER = "g/m³" UNIT_HECTOPASCAL = "hPa" UNIT_HERTZ = "Hz" UNIT_HOUR = "h" diff --git a/tests/test1.yaml b/tests/test1.yaml index 4312f2c613..8180400730 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1229,6 +1229,10 @@ sensor: model: 1005 update_interval: 60s i2c_id: i2c_bus + - platform: absolute_humidity + name: DHT Absolute Humidity + temperature: dht_temperature + humidity: dht_humidity esp32_touch: setup_mode: false From ceebe1462884fcec3c1453abd5c887c087377c02 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 7 Mar 2023 22:29:45 +0100 Subject: [PATCH 085/115] Add ability to await safe mode in codegen (#4529) * Add ability to await OTA safe mode * Make pylint happy --- esphome/codegen.py | 1 + esphome/components/ota/__init__.py | 5 +++++ esphome/const.py | 1 + esphome/cpp_helpers.py | 20 ++++++++++++++++++++ 4 files changed, 27 insertions(+) diff --git a/esphome/codegen.py b/esphome/codegen.py index c926c94070..43b44256e2 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -47,6 +47,7 @@ from esphome.cpp_helpers import ( # noqa build_registry_list, extract_registry_entry_config, register_parented, + past_safe_mode, ) from esphome.cpp_types import ( # noqa global_ns, diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 32ea1fd363..a966157ffa 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, CONF_TRIGGER_ID, + CONF_OTA, + KEY_PAST_SAFE_MODE, ) from esphome.core import CORE, coroutine_with_priority @@ -76,6 +78,8 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(50.0) async def to_code(config): + CORE.data[CONF_OTA] = {} + var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_port(config[CONF_PORT])) cg.add_define("USE_OTA") @@ -90,6 +94,7 @@ async def to_code(config): config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT] ) cg.add(RawExpression(f"if ({condition}) return")) + CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) diff --git a/esphome/const.py b/esphome/const.py index 289a59b424..4d6c7623bc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1023,6 +1023,7 @@ KEY_TARGET_FRAMEWORK = "target_framework" KEY_FRAMEWORK_VERSION = "framework_version" KEY_NAME = "name" KEY_VARIANT = "variant" +KEY_PAST_SAFE_MODE = "past_safe_mode" # Entity categories ENTITY_CATEGORY_NONE = "" diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 02d339441f..ab5231e055 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,9 +9,13 @@ from esphome.const import ( CONF_SETUP_PRIORITY, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, + CONF_OTA, + CONF_SAFE_MODE, + KEY_PAST_SAFE_MODE, ) from esphome.core import coroutine, ID, CORE +from esphome.coroutine import FakeAwaitable from esphome.types import ConfigType, ConfigFragmentType from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App @@ -127,3 +131,19 @@ async def build_registry_list(registry, config): action = await build_registry_entry(registry, conf) actions.append(action) return actions + + +async def past_safe_mode(): + safe_mode_enabled = ( + CONF_OTA in CORE.config and CORE.config[CONF_OTA][CONF_SAFE_MODE] + ) + if not safe_mode_enabled: + return + + def _safe_mode_generator(): + while True: + if CORE.data.get(CONF_OTA, {}).get(KEY_PAST_SAFE_MODE, False): + return + yield + + return await FakeAwaitable(_safe_mode_generator()) From 623e31ddeef1d507446e028e3d9d6ab5443f903e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Mar 2023 14:11:12 +1300 Subject: [PATCH 086/115] sn74hc165 fixes (#4457) * Add delay between clock changes on sn74hc165 * Increase to 10us * Add another delay after clock low * Print input bits every second * Fix pin order * Remove log * Fix for inverted pins * formatting --- esphome/components/sn74hc165/sn74hc165.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp index 6c89544db4..7efe8a4c14 100644 --- a/esphome/components/sn74hc165/sn74hc165.cpp +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -40,17 +40,22 @@ bool SN74HC165Component::digital_read_(uint16_t pin) { void SN74HC165Component::read_gpio_() { this->load_pin_->digital_write(false); - delayMicroseconds(5); + delayMicroseconds(10); this->load_pin_->digital_write(true); - delayMicroseconds(5); + delayMicroseconds(10); if (this->clock_inhibit_pin_ != nullptr) this->clock_inhibit_pin_->digital_write(false); - for (int16_t i = (this->sr_count_ * 8) - 1; i >= 0; i--) { - this->input_bits_[i] = this->data_pin_->digital_read(); - this->clock_pin_->digital_write(true); - this->clock_pin_->digital_write(false); + for (uint8_t i = 0; i < this->sr_count_; i++) { + for (uint8_t j = 0; j < 8; j++) { + this->input_bits_[(i * 8) + (7 - j)] = this->data_pin_->digital_read(); + + this->clock_pin_->digital_write(true); + delayMicroseconds(10); + this->clock_pin_->digital_write(false); + delayMicroseconds(10); + } } if (this->clock_inhibit_pin_ != nullptr) @@ -59,7 +64,7 @@ void SN74HC165Component::read_gpio_() { float SN74HC165Component::get_setup_priority() const { return setup_priority::IO; } -bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_); } +bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } std::string SN74HC165GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC165", 18, pin_); } From bc28ea1fdec0febec3646d4794578f3a5bd3ae23 Mon Sep 17 00:00:00 2001 From: Morgan Robertson Date: Wed, 8 Mar 2023 01:15:49 +0000 Subject: [PATCH 087/115] Add AS7341 spectral color sensor (#4331) * Add support for AS7341 spectral color sensor. * Run clang-format and clang-tidy. * Post-review changes. * Apply suggestions from code review --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/as7341/__init__.py | 0 esphome/components/as7341/as7341.cpp | 271 ++++++++++++++++++++++++++ esphome/components/as7341/as7341.h | 144 ++++++++++++++ esphome/components/as7341/sensor.py | 112 +++++++++++ tests/test1.yaml | 26 +++ 6 files changed, 554 insertions(+) create mode 100644 esphome/components/as7341/__init__.py create mode 100644 esphome/components/as7341/as7341.cpp create mode 100644 esphome/components/as7341/as7341.h create mode 100644 esphome/components/as7341/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1ff9661add..3d6ea5cd32 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -25,6 +25,7 @@ esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter +esphome/components/as7341/* @mrgnr esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron diff --git a/esphome/components/as7341/__init__.py b/esphome/components/as7341/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp new file mode 100644 index 0000000000..129a3f9e37 --- /dev/null +++ b/esphome/components/as7341/as7341.cpp @@ -0,0 +1,271 @@ +#include "as7341.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace as7341 { + +static const char *const TAG = "as7341"; + +void AS7341Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AS7341..."); + LOG_I2C_DEVICE(this); + + // Verify device ID + uint8_t id; + this->read_byte(AS7341_ID, &id); + ESP_LOGCONFIG(TAG, " Read ID: 0x%X", id); + if ((id & 0xFC) != (AS7341_CHIP_ID << 2)) { + this->mark_failed(); + return; + } + + // Power on (enter IDLE state) + if (!this->enable_power(true)) { + ESP_LOGE(TAG, " Power on failed!"); + this->mark_failed(); + return; + } + + // Set configuration + this->write_byte(AS7341_CONFIG, 0x00); + this->setup_atime(this->atime_); + this->setup_astep(this->astep_); + this->setup_gain(this->gain_); +} + +void AS7341Component::dump_config() { + ESP_LOGCONFIG(TAG, "AS7341:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AS7341 failed!"); + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Gain: %u", get_gain()); + ESP_LOGCONFIG(TAG, " ATIME: %u", get_atime()); + ESP_LOGCONFIG(TAG, " ASTEP: %u", get_astep()); + + LOG_SENSOR(" ", "F1", this->f1_); + LOG_SENSOR(" ", "F2", this->f2_); + LOG_SENSOR(" ", "F3", this->f3_); + LOG_SENSOR(" ", "F4", this->f4_); + LOG_SENSOR(" ", "F5", this->f5_); + LOG_SENSOR(" ", "F6", this->f6_); + LOG_SENSOR(" ", "F7", this->f7_); + LOG_SENSOR(" ", "F8", this->f8_); + LOG_SENSOR(" ", "Clear", this->clear_); + LOG_SENSOR(" ", "NIR", this->nir_); +} + +float AS7341Component::get_setup_priority() const { return setup_priority::DATA; } + +void AS7341Component::update() { + this->read_channels(this->channel_readings_); + + if (this->f1_ != nullptr) { + this->f1_->publish_state(this->channel_readings_[0]); + } + if (this->f2_ != nullptr) { + this->f2_->publish_state(this->channel_readings_[1]); + } + if (this->f3_ != nullptr) { + this->f3_->publish_state(this->channel_readings_[2]); + } + if (this->f4_ != nullptr) { + this->f4_->publish_state(this->channel_readings_[3]); + } + if (this->f5_ != nullptr) { + this->f5_->publish_state(this->channel_readings_[6]); + } + if (this->f6_ != nullptr) { + this->f6_->publish_state(this->channel_readings_[7]); + } + if (this->f7_ != nullptr) { + this->f7_->publish_state(this->channel_readings_[8]); + } + if (this->f8_ != nullptr) { + this->f8_->publish_state(this->channel_readings_[9]); + } + if (this->clear_ != nullptr) { + this->clear_->publish_state(this->channel_readings_[10]); + } + if (this->nir_ != nullptr) { + this->nir_->publish_state(this->channel_readings_[11]); + } +} + +AS7341Gain AS7341Component::get_gain() { + uint8_t data; + this->read_byte(AS7341_CFG1, &data); + return (AS7341Gain) data; +} + +uint8_t AS7341Component::get_atime() { + uint8_t data; + this->read_byte(AS7341_ATIME, &data); + return data; +} + +uint16_t AS7341Component::get_astep() { + uint16_t data; + this->read_byte_16(AS7341_ASTEP, &data); + return this->swap_bytes(data); +} + +bool AS7341Component::setup_gain(AS7341Gain gain) { return this->write_byte(AS7341_CFG1, gain); } + +bool AS7341Component::setup_atime(uint8_t atime) { return this->write_byte(AS7341_ATIME, atime); } + +bool AS7341Component::setup_astep(uint16_t astep) { return this->write_byte_16(AS7341_ASTEP, swap_bytes(astep)); } + +bool AS7341Component::read_channels(uint16_t *data) { + this->set_smux_low_channels(true); + this->enable_spectral_measurement(true); + this->wait_for_data(); + bool low_success = this->read_bytes_16(AS7341_CH0_DATA_L, data, 6); + + this->set_smux_low_channels(false); + this->enable_spectral_measurement(true); + this->wait_for_data(); + bool high_sucess = this->read_bytes_16(AS7341_CH0_DATA_L, &data[6], 6); + + return low_success && high_sucess; +} + +void AS7341Component::set_smux_low_channels(bool enable) { + this->enable_spectral_measurement(false); + this->set_smux_command(AS7341_SMUX_CMD_WRITE); + + if (enable) { + this->configure_smux_low_channels(); + + } else { + this->configure_smux_high_channels(); + } + this->enable_smux(); +} + +bool AS7341Component::set_smux_command(AS7341SmuxCommand command) { + uint8_t data = command << 3; // Write to bits 4:3 of the register + return this->write_byte(AS7341_CFG6, data); +} + +void AS7341Component::configure_smux_low_channels() { + // SMUX Config for F1,F2,F3,F4,NIR,Clear + this->write_byte(0x00, 0x30); // F3 left set to ADC2 + this->write_byte(0x01, 0x01); // F1 left set to ADC0 + this->write_byte(0x02, 0x00); // Reserved or disabled + this->write_byte(0x03, 0x00); // F8 left disabled + this->write_byte(0x04, 0x00); // F6 left disabled + this->write_byte(0x05, 0x42); // F4 left connected to ADC3/f2 left connected to ADC1 + this->write_byte(0x06, 0x00); // F5 left disbled + this->write_byte(0x07, 0x00); // F7 left disbled + this->write_byte(0x08, 0x50); // CLEAR connected to ADC4 + this->write_byte(0x09, 0x00); // F5 right disabled + this->write_byte(0x0A, 0x00); // F7 right disabled + this->write_byte(0x0B, 0x00); // Reserved or disabled + this->write_byte(0x0C, 0x20); // F2 right connected to ADC1 + this->write_byte(0x0D, 0x04); // F4 right connected to ADC3 + this->write_byte(0x0E, 0x00); // F6/F8 right disabled + this->write_byte(0x0F, 0x30); // F3 right connected to AD2 + this->write_byte(0x10, 0x01); // F1 right connected to AD0 + this->write_byte(0x11, 0x50); // CLEAR right connected to AD4 + this->write_byte(0x12, 0x00); // Reserved or disabled + this->write_byte(0x13, 0x06); // NIR connected to ADC5 +} + +void AS7341Component::configure_smux_high_channels() { + // SMUX Config for F5,F6,F7,F8,NIR,Clear + this->write_byte(0x00, 0x00); // F3 left disable + this->write_byte(0x01, 0x00); // F1 left disable + this->write_byte(0x02, 0x00); // reserved/disable + this->write_byte(0x03, 0x40); // F8 left connected to ADC3 + this->write_byte(0x04, 0x02); // F6 left connected to ADC1 + this->write_byte(0x05, 0x00); // F4/ F2 disabled + this->write_byte(0x06, 0x10); // F5 left connected to ADC0 + this->write_byte(0x07, 0x03); // F7 left connected to ADC2 + this->write_byte(0x08, 0x50); // CLEAR Connected to ADC4 + this->write_byte(0x09, 0x10); // F5 right connected to ADC0 + this->write_byte(0x0A, 0x03); // F7 right connected to ADC2 + this->write_byte(0x0B, 0x00); // Reserved or disabled + this->write_byte(0x0C, 0x00); // F2 right disabled + this->write_byte(0x0D, 0x00); // F4 right disabled + this->write_byte(0x0E, 0x24); // F8 right connected to ADC2/ F6 right connected to ADC1 + this->write_byte(0x0F, 0x00); // F3 right disabled + this->write_byte(0x10, 0x00); // F1 right disabled + this->write_byte(0x11, 0x50); // CLEAR right connected to AD4 + this->write_byte(0x12, 0x00); // Reserved or disabled + this->write_byte(0x13, 0x06); // NIR connected to ADC5 +} + +bool AS7341Component::enable_smux() { + this->set_register_bit(AS7341_ENABLE, 4); + + uint16_t timeout = 1000; + for (uint16_t time = 0; time < timeout; time++) { + // The SMUXEN bit is cleared once the SMUX operation is finished + bool smuxen = this->read_register_bit(AS7341_ENABLE, 4); + if (!smuxen) { + return true; + } + + delay(1); + } + + return false; +} + +bool AS7341Component::wait_for_data() { + uint16_t timeout = 1000; + for (uint16_t time = 0; time < timeout; time++) { + if (this->is_data_ready()) { + return true; + } + + delay(1); + } + + return false; +} + +bool AS7341Component::is_data_ready() { return this->read_register_bit(AS7341_STATUS2, 6); } + +bool AS7341Component::enable_power(bool enable) { return this->write_register_bit(AS7341_ENABLE, enable, 0); } + +bool AS7341Component::enable_spectral_measurement(bool enable) { + return this->write_register_bit(AS7341_ENABLE, enable, 1); +} + +bool AS7341Component::read_register_bit(uint8_t address, uint8_t bit_position) { + uint8_t data; + this->read_byte(address, &data); + bool bit = (data & (1 << bit_position)) > 0; + return bit; +} + +bool AS7341Component::write_register_bit(uint8_t address, bool value, uint8_t bit_position) { + if (value) { + return this->set_register_bit(address, bit_position); + } + + return this->clear_register_bit(address, bit_position); +} + +bool AS7341Component::set_register_bit(uint8_t address, uint8_t bit_position) { + uint8_t data; + this->read_byte(address, &data); + data |= (1 << bit_position); + return this->write_byte(address, data); +} + +bool AS7341Component::clear_register_bit(uint8_t address, uint8_t bit_position) { + uint8_t data; + this->read_byte(address, &data); + data &= ~(1 << bit_position); + return this->write_byte(address, data); +} + +uint16_t AS7341Component::swap_bytes(uint16_t data) { return (data >> 8) | (data << 8); } + +} // namespace as7341 +} // namespace esphome diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h new file mode 100644 index 0000000000..e517e1d2bf --- /dev/null +++ b/esphome/components/as7341/as7341.h @@ -0,0 +1,144 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace as7341 { + +static const uint8_t AS7341_CHIP_ID = 0X09; + +static const uint8_t AS7341_CONFIG = 0x70; +static const uint8_t AS7341_LED = 0x74; + +static const uint8_t AS7341_ENABLE = 0x80; +static const uint8_t AS7341_ATIME = 0x81; + +static const uint8_t AS7341_WTIME = 0x83; + +static const uint8_t AS7341_AUXID = 0x90; +static const uint8_t AS7341_REVID = 0x91; +static const uint8_t AS7341_ID = 0x92; +static const uint8_t AS7341_STATUS = 0x93; + +static const uint8_t AS7341_CH0_DATA_L = 0x95; +static const uint8_t AS7341_CH0_DATA_H = 0x96; +static const uint8_t AS7341_CH1_DATA_L = 0x97; +static const uint8_t AS7341_CH1_DATA_H = 0x98; +static const uint8_t AS7341_CH2_DATA_L = 0x99; +static const uint8_t AS7341_CH2_DATA_H = 0x9A; +static const uint8_t AS7341_CH3_DATA_L = 0x9B; +static const uint8_t AS7341_CH3_DATA_H = 0x9C; +static const uint8_t AS7341_CH4_DATA_L = 0x9D; +static const uint8_t AS7341_CH4_DATA_H = 0x9E; +static const uint8_t AS7341_CH5_DATA_L = 0x9F; +static const uint8_t AS7341_CH5_DATA_H = 0xA0; + +static const uint8_t AS7341_STATUS2 = 0xA3; + +static const uint8_t AS7341_CFG1 = 0xAA; ///< Controls ADC Gain + +static const uint8_t AS7341_CFG6 = 0xAF; // Stores SMUX command +static const uint8_t AS7341_CFG9 = 0xB2; // Config for system interrupts (SMUX, Flicker detection) + +static const uint8_t AS7341_ASTEP = 0xCA; // LSB +static const uint8_t AS7341_ASTEP_MSB = 0xCB; // MSB + +enum AS7341AdcChannel { + AS7341_ADC_CHANNEL_0, + AS7341_ADC_CHANNEL_1, + AS7341_ADC_CHANNEL_2, + AS7341_ADC_CHANNEL_3, + AS7341_ADC_CHANNEL_4, + AS7341_ADC_CHANNEL_5, +}; + +enum AS7341SmuxCommand { + AS7341_SMUX_CMD_ROM_RESET, ///< ROM code initialization of SMUX + AS7341_SMUX_CMD_READ, ///< Read SMUX configuration to RAM from SMUX chain + AS7341_SMUX_CMD_WRITE, ///< Write SMUX configuration from RAM to SMUX chain +}; + +enum AS7341Gain { + AS7341_GAIN_0_5X, + AS7341_GAIN_1X, + AS7341_GAIN_2X, + AS7341_GAIN_4X, + AS7341_GAIN_8X, + AS7341_GAIN_16X, + AS7341_GAIN_32X, + AS7341_GAIN_64X, + AS7341_GAIN_128X, + AS7341_GAIN_256X, + AS7341_GAIN_512X, +}; + +class AS7341Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_f1_sensor(sensor::Sensor *f1_sensor) { this->f1_ = f1_sensor; } + void set_f2_sensor(sensor::Sensor *f2_sensor) { f2_ = f2_sensor; } + void set_f3_sensor(sensor::Sensor *f3_sensor) { f3_ = f3_sensor; } + void set_f4_sensor(sensor::Sensor *f4_sensor) { f4_ = f4_sensor; } + void set_f5_sensor(sensor::Sensor *f5_sensor) { f5_ = f5_sensor; } + void set_f6_sensor(sensor::Sensor *f6_sensor) { f6_ = f6_sensor; } + void set_f7_sensor(sensor::Sensor *f7_sensor) { f7_ = f7_sensor; } + void set_f8_sensor(sensor::Sensor *f8_sensor) { f8_ = f8_sensor; } + void set_clear_sensor(sensor::Sensor *clear_sensor) { clear_ = clear_sensor; } + void set_nir_sensor(sensor::Sensor *nir_sensor) { nir_ = nir_sensor; } + + void set_gain(AS7341Gain gain) { gain_ = gain; } + void set_atime(uint8_t atime) { atime_ = atime; } + void set_astep(uint16_t astep) { astep_ = astep; } + + AS7341Gain get_gain(); + uint8_t get_atime(); + uint16_t get_astep(); + bool setup_gain(AS7341Gain gain); + bool setup_atime(uint8_t atime); + bool setup_astep(uint16_t astep); + + uint16_t read_channel(AS7341AdcChannel channel); + bool read_channels(uint16_t *data); + void set_smux_low_channels(bool enable); + bool set_smux_command(AS7341SmuxCommand command); + void configure_smux_low_channels(); + void configure_smux_high_channels(); + bool enable_smux(); + + bool wait_for_data(); + bool is_data_ready(); + bool enable_power(bool enable); + bool enable_spectral_measurement(bool enable); + + bool read_register_bit(uint8_t address, uint8_t bit_position); + bool write_register_bit(uint8_t address, bool value, uint8_t bit_position); + bool set_register_bit(uint8_t address, uint8_t bit_position); + bool clear_register_bit(uint8_t address, uint8_t bit_position); + uint16_t swap_bytes(uint16_t data); + + protected: + sensor::Sensor *f1_{nullptr}; + sensor::Sensor *f2_{nullptr}; + sensor::Sensor *f3_{nullptr}; + sensor::Sensor *f4_{nullptr}; + sensor::Sensor *f5_{nullptr}; + sensor::Sensor *f6_{nullptr}; + sensor::Sensor *f7_{nullptr}; + sensor::Sensor *f8_{nullptr}; + sensor::Sensor *clear_{nullptr}; + sensor::Sensor *nir_{nullptr}; + + uint16_t astep_; + AS7341Gain gain_; + uint8_t atime_; + uint16_t channel_readings_[12]; +}; + +} // namespace as7341 +} // namespace esphome diff --git a/esphome/components/as7341/sensor.py b/esphome/components/as7341/sensor.py new file mode 100644 index 0000000000..2424087c35 --- /dev/null +++ b/esphome/components/as7341/sensor.py @@ -0,0 +1,112 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + CONF_ID, + DEVICE_CLASS_ILLUMINANCE, + ICON_BRIGHTNESS_5, + STATE_CLASS_MEASUREMENT, +) + + +CODEOWNERS = ["@mrgnr"] +DEPENDENCIES = ["i2c"] + +as7341_ns = cg.esphome_ns.namespace("as7341") + +AS7341Component = as7341_ns.class_( + "AS7341Component", cg.PollingComponent, i2c.I2CDevice +) + +CONF_ATIME = "atime" +CONF_ASTEP = "astep" + +CONF_F1 = "f1" +CONF_F2 = "f2" +CONF_F3 = "f3" +CONF_F4 = "f4" +CONF_F5 = "f5" +CONF_F6 = "f6" +CONF_F7 = "f7" +CONF_F8 = "f8" +CONF_CLEAR = "clear" +CONF_NIR = "nir" + +UNIT_COUNTS = "#" + +AS7341_GAIN = as7341_ns.enum("AS7341Gain") +GAIN_OPTIONS = { + "X0.5": AS7341_GAIN.AS7341_GAIN_0_5X, + "X1": AS7341_GAIN.AS7341_GAIN_1X, + "X2": AS7341_GAIN.AS7341_GAIN_2X, + "X4": AS7341_GAIN.AS7341_GAIN_4X, + "X8": AS7341_GAIN.AS7341_GAIN_8X, + "X16": AS7341_GAIN.AS7341_GAIN_16X, + "X32": AS7341_GAIN.AS7341_GAIN_32X, + "X64": AS7341_GAIN.AS7341_GAIN_64X, + "X128": AS7341_GAIN.AS7341_GAIN_128X, + "X256": AS7341_GAIN.AS7341_GAIN_256X, + "X512": AS7341_GAIN.AS7341_GAIN_512X, +} + + +SENSOR_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, +) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AS7341Component), + cv.Optional(CONF_F1): SENSOR_SCHEMA, + cv.Optional(CONF_F2): SENSOR_SCHEMA, + cv.Optional(CONF_F3): SENSOR_SCHEMA, + cv.Optional(CONF_F4): SENSOR_SCHEMA, + cv.Optional(CONF_F5): SENSOR_SCHEMA, + cv.Optional(CONF_F6): SENSOR_SCHEMA, + cv.Optional(CONF_F7): SENSOR_SCHEMA, + cv.Optional(CONF_F8): SENSOR_SCHEMA, + cv.Optional(CONF_CLEAR): SENSOR_SCHEMA, + cv.Optional(CONF_NIR): SENSOR_SCHEMA, + cv.Optional(CONF_GAIN, default="X8"): cv.enum(GAIN_OPTIONS), + cv.Optional(CONF_ATIME, default=29): cv.int_range(min=0, max=255), + cv.Optional(CONF_ASTEP, default=599): cv.int_range(min=0, max=65534), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x39)) +) + +SENSORS = { + CONF_F1: "set_f1_sensor", + CONF_F2: "set_f2_sensor", + CONF_F3: "set_f3_sensor", + CONF_F4: "set_f4_sensor", + CONF_F5: "set_f5_sensor", + CONF_F6: "set_f6_sensor", + CONF_F7: "set_f7_sensor", + CONF_F8: "set_f8_sensor", + CONF_CLEAR: "set_clear_sensor", + CONF_NIR: "set_nir_sensor", +} + + +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_gain(config[CONF_GAIN])) + cg.add(var.set_atime(config[CONF_ATIME])) + cg.add(var.set_astep(config[CONF_ASTEP])) + + for conf_id, set_sensor_func in SENSORS.items(): + if conf_id in config: + sens = await sensor.new_sensor(config[conf_id]) + cg.add(getattr(var, set_sensor_func)(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 8180400730..b599eb2666 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -439,6 +439,32 @@ sensor: state_topic: hi/me retain: false availability: + - platform: as7341 + update_interval: 15s + gain: X8 + atime: 120 + astep: 99 + f1: + name: F1 + f2: + name: F2 + f3: + name: F3 + f4: + name: F4 + f5: + name: F5 + f6: + name: F6 + f7: + name: F7 + f8: + name: F8 + clear: + name: Clear + nir: + name: NIR + i2c_id: i2c_bus - platform: atm90e32 cs_pin: 5 phase_a: From 2ef25f31532bedc302e7cf8aa669b5eeba67c8fe Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Mar 2023 20:12:07 +1300 Subject: [PATCH 088/115] Fix ethernet driver setting gpio 5 high when no power pin defined (#4531) --- esphome/components/ethernet/ethernet_component.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index a3f0ae715f..9152b33a14 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -43,8 +43,7 @@ void EthernetComponent::setup() { 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_; mac_config.smi_mdc_gpio_num = this->mdc_pin_; mac_config.smi_mdio_gpio_num = this->mdio_pin_; From 5e6665494d739e339dda45c52ce756a22f9df227 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 8 Mar 2023 19:23:02 +0100 Subject: [PATCH 089/115] Use PSRam for BLE scan results. (#4486) * Use PSRam for BLE scan results. * Format Document * Use generic define `CONFIG_SPIRAM`. * Formatting changes. * Memory allocation is allowed to fail. * Use mark_failed instead of abort. --------- Co-authored-by: Your Name Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 12 ++++++++++-- .../components/esp32_ble_tracker/esp32_ble_tracker.h | 9 +++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 6b0f4dc897..26687ba9cc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -53,6 +53,14 @@ void ESP32BLETracker::setup() { ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); return; } + ExternalRAMAllocator allocator( + ExternalRAMAllocator::ALLOW_FAILURE); + this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE); + + if (this->scan_result_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); + this->mark_failed(); + } global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); @@ -107,7 +115,7 @@ void ESP32BLETracker::loop() { xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { uint32_t index = this->scan_result_index_; if (index) { - if (index >= 16) { + if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); } for (size_t i = 0; i < index; i++) { @@ -322,7 +330,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { - if (this->scan_result_index_ < 16) { + if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { this->scan_result_buffer_[this->scan_result_index_++] = param; } xSemaphoreGive(this->scan_result_lock_); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index d1f72cf78d..798f68f2bd 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -101,7 +101,7 @@ class ESPBTDevice { std::vector tx_powers_{}; optional appearance_{}; optional ad_flag_{}; - std::vector service_uuids_; + std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; @@ -231,7 +231,12 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; - esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; +#if CONFIG_SPIRAM + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; +#else + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16; +#endif // CONFIG_SPIRAM + esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; }; From 600f4be2c424e0f15376406981c3198e09f6b039 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Mar 2023 08:25:25 -1000 Subject: [PATCH 090/115] Bump esp-idf to 4.4.4 (#4528) There are some nice BLE fixes and this uses about ~5000-8000 bytes less RAM https://github.com/espressif/esp-idf/releases/tag/v4.4.4 --- esphome/components/esp32/__init__.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ba11aeef21..62021afea9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -168,7 +168,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 2, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 3) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 4) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 diff --git a/platformio.ini b/platformio.ini index f2588e918a..65dccde3b9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -135,7 +135,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = platformio/espressif32 @ 5.3.0 platform_packages = - platformio/framework-espidf @ ~3.40403.0 + platformio/framework-espidf @ ~3.40404.0 framework = espidf lib_deps = From 1087cb55b4b04a6ca1e0c2fc31e47df9404b10df Mon Sep 17 00:00:00 2001 From: Kai Gerken Date: Wed, 8 Mar 2023 20:00:44 +0100 Subject: [PATCH 091/115] Added pzemdc reset energy action (#4481) * remove unused sensors on pzemdc * add reset energy action for pzemdc * fix lint errors * remove trailing space on pzemdc.h * make method reset_energy of pzemdc public * Apply suggestions from code review --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/pzemdc/pzemdc.cpp | 8 ++++++++ esphome/components/pzemdc/pzemdc.h | 16 ++++++++++++---- esphome/components/pzemdc/sensor.py | 19 +++++++++++++++++++ tests/test3.yaml | 6 ++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index c914edefc8..28e4210ff7 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -7,6 +7,7 @@ namespace pzemdc { static const char *const TAG = "pzemdc"; static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers void PZEMDC::on_modbus_data(const std::vector &data) { @@ -61,5 +62,12 @@ void PZEMDC::dump_config() { LOG_SENSOR("", "Energy", this->energy_sensor_); } +void PZEMDC::reset_energy() { + std::vector cmd; + cmd.push_back(this->address_); + cmd.push_back(PZEM_CMD_RESET_ENERGY); + this->send_raw(cmd); +} + } // namespace pzemdc } // namespace esphome diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index 54df6c484c..21676e3422 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -15,8 +15,6 @@ class PZEMDC : public PollingComponent, public modbus::ModbusDevice { void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } - void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } - void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } void update() override; @@ -24,14 +22,24 @@ class PZEMDC : public PollingComponent, public modbus::ModbusDevice { void dump_config() override; + void reset_energy(); + protected: sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; - sensor::Sensor *frequency_sensor_{nullptr}; - sensor::Sensor *power_factor_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; }; +template class ResetEnergyAction : public Action { + public: + ResetEnergyAction(PZEMDC *pzemdc) : pzemdc_(pzemdc) {} + + void play(Ts... x) override { this->pzemdc_->reset_energy(); } + + protected: + PZEMDC *pzemdc_; +}; + } // namespace pzemdc } // namespace esphome diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 4234ce19ad..097b1c1cfd 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -1,5 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id from esphome.components import sensor, modbus from esphome.const import ( CONF_CURRENT, @@ -24,6 +26,9 @@ AUTO_LOAD = ["modbus"] pzemdc_ns = cg.esphome_ns.namespace("pzemdc") PZEMDC = pzemdc_ns.class_("PZEMDC", cg.PollingComponent, modbus.ModbusDevice) +# Actions +ResetEnergyAction = pzemdc_ns.class_("ResetEnergyAction", automation.Action) + CONFIG_SCHEMA = ( cv.Schema( { @@ -59,6 +64,20 @@ CONFIG_SCHEMA = ( ) +@automation.register_action( + "pzemdc.reset_energy", + ResetEnergyAction, + maybe_simple_id( + { + cv.GenerateID(CONF_ID): cv.use_id(PZEMDC), + } + ), +) +async def reset_energy_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/tests/test3.yaml b/tests/test3.yaml index e62a171c95..f1b64b3389 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -579,6 +579,7 @@ sensor: power_factor: name: PZEMAC Power Factor - platform: pzemdc + id: pzemdc1 voltage: name: PZEMDC Voltage current: @@ -925,6 +926,11 @@ binary_sensor: on_press: then: - pzemac.reset_energy: pzemac1 + - platform: template + id: pzemdc_reset_energy + on_press: + then: + - pzemdc.reset_energy: pzemdc1 - platform: vbus model: deltasol_bs_plus From f58ffe41f8f681741e09ca466d594e3d0b841da3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Mar 2023 09:09:39 +1300 Subject: [PATCH 092/115] Bump version to 2023.3.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4d6c7623bc..a78fb6948f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.3.0-dev" +__version__ = "2023.3.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From fb5eb57345ab0f9c84a9acf276987d2fff12f7bc Mon Sep 17 00:00:00 2001 From: Jared Sanson Date: Thu, 9 Mar 2023 10:03:11 +1300 Subject: [PATCH 093/115] Fix ethernet clk_mode for GPIO0_OUT (#4307) * Fix GPIO0_OUT definition for ethernet component * Formatting --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ethernet/__init__.py | 23 +++++++++++++++---- .../ethernet/ethernet_component.cpp | 9 +++++--- .../components/ethernet/ethernet_component.h | 5 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index a0f8b557b0..f6ca376681 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -36,12 +36,25 @@ ETHERNET_TYPES = { "JL1101": EthernetType.ETHERNET_TYPE_JL1101, } +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, + ), } @@ -114,7 +127,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/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 9152b33a14..7120223cc9 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -47,8 +47,8 @@ void EthernetComponent::setup() { 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); @@ -315,7 +315,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 { diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index a538a5c77d..872ed17044 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -50,7 +50,7 @@ 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(); @@ -70,7 +70,8 @@ class EthernetComponent : public Component { 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}; + 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}; From 2bed5b18c15ed11e0d0236a1d2ec7200dc3d4cf5 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Wed, 8 Mar 2023 14:35:40 -0800 Subject: [PATCH 094/115] Add ESP32-S3 support in NeoPixelBus component (#4114) * Add ESP32-S3 support in NeoPixelBus component * Update NeoPixelBus version in platformio.ini --- esphome/components/neopixelbus/_methods.py | 6 +++--- esphome/components/neopixelbus/light.py | 3 ++- platformio.ini | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py index a290257d6b..4059d040d0 100644 --- a/esphome/components/neopixelbus/_methods.py +++ b/esphome/components/neopixelbus/_methods.py @@ -13,8 +13,8 @@ from esphome.const import ( from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, - VARIANT_ESP32S2, VARIANT_ESP32C3, + VARIANT_ESP32S2, VARIANT_ESP32S3, ) from esphome.core import CORE @@ -58,9 +58,9 @@ SPI_SPEEDS = [40e6, 20e6, 10e6, 5e6, 2e6, 1e6, 500e3] def _esp32_rmt_default_channel(): return { + VARIANT_ESP32C3: 1, VARIANT_ESP32S2: 1, VARIANT_ESP32S3: 1, - VARIANT_ESP32C3: 1, }.get(get_esp32_variant(), 6) @@ -71,9 +71,9 @@ def _validate_esp32_rmt_channel(value): value = cv.int_(value) variant_channels = { VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7, CHANNEL_DYNAMIC], + VARIANT_ESP32C3: [0, 1, CHANNEL_DYNAMIC], VARIANT_ESP32S2: [0, 1, 2, 3, CHANNEL_DYNAMIC], VARIANT_ESP32S3: [0, 1, 2, 3, CHANNEL_DYNAMIC], - VARIANT_ESP32C3: [0, 1, CHANNEL_DYNAMIC], } variant = get_esp32_variant() if variant not in variant_channels: diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 072a565eda..9bd9215936 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -17,6 +17,7 @@ from esphome.const import ( from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32C3, + VARIANT_ESP32S3, ) from esphome.core import CORE from ._methods import ( @@ -96,7 +97,7 @@ def _choose_default_method(config): config[CONF_METHOD] = _validate_method(METHOD_BIT_BANG) if CORE.is_esp32: - if get_esp32_variant() == VARIANT_ESP32C3: + if get_esp32_variant() in (VARIANT_ESP32C3, VARIANT_ESP32S3): config[CONF_METHOD] = _validate_method(METHOD_ESP32_RMT) else: config[CONF_METHOD] = _validate_method(METHOD_ESP32_I2S) diff --git a/platformio.ini b/platformio.ini index 65dccde3b9..3df6446ff1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,7 +34,7 @@ build_flags = [common] lib_deps = esphome/noise-c@0.1.4 ; api - makuna/NeoPixelBus@2.6.9 ; neopixelbus + makuna/NeoPixelBus@2.7.3 ; neopixelbus esphome/Improv@1.2.3 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code From 58a8e1859e3c9c63afd13a01bb17ec4b7c686ae8 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Thu, 9 Mar 2023 00:03:49 +0100 Subject: [PATCH 095/115] Renaming and extending the ili9341 to the ili9xxx component (#4275) * - Removed cleaning the screen twice. \Should be handled by `DisplayBuffer::init_internal_();` - Made ili9341::initalize() protected and renamed \ it to ili9341::initalize(). - ili9341::initalize() should only init the display \ and set the width and heigth. * removed to much * clang format fixes * removing trailing underscors for protected virtual methods * removed the "override" on display() * clang fixes * restored old changes * Renamed the ili9341 platform to ili9xxx and added multiple drivers as well. including PR #3848 * fixed most of the clang reported issues * fixed reported issues * last fixes * Setting the right codeowners * missing changes * fixed naming Display() method. * clang again * clang fix * fixes reported by @jesserockz & @gpambrozio * a change to display.py removing an unneeded var * re-introduce **backlight** option. * update the ili9488 initialization * update the ili9488 initialization and fix typo * fixed typo * add missing constants * swap height and width back for the ili9488 * init fixes ili9488 * fixed lint issue testing the init code * oeps * init fixes ili9488 * fixed wrong define * fixed wrong define again * removed some spaces * revert to ili9341 * Remove parts that where used for the switchplate * lint fixes and removing unused function * fix error and introducing 16bit color option * fix error and introducing 16bit color option * fix clang issue * clang fix * clang issue again * is this what clang exprect * clang fix * clang fix * try again * let try again * and again * and the last clang fix * remove the need of wifi * update dimentions * update ili8488 init code. * update dimentions * allow to change height and width * dump color mode config * fix * fix * modify logging * referd back unrelated change * code formatting commit and moving functions around * add missing ; * update code * update code * use the correct write_array for sending uint16_t * fix panic loop * fix panic loop * - update the test file - fixed sending display data * clang fixes * clang fixes * clang fixes again * remove .gitignore items * remove .gitignore items * make sure Update() can can not be called while called * clang correction * adding a test yaml for the ili9341 * Update ili9341 example * Make test ili9xxx/tests only local * restore back old ili9341 driver code * Add a new config for the M5Core * fix clang request * reverd to restore of the old ili9341 there is no proper way to say it is depricated. * Remove the backlight/led pin from the config. You need to use a proper light platform component * Ili9488init changes (#88) Fixed ILI9488 init settings, and adjusted pixel handling code to push pixels in 18 bit format. This does not change the internal 16-bit representation. * fixed some leftover clang issues from the merge. * fixed the slang-tidy request. * remove `backlight_pin` warning. --------- Co-authored-by: JD Steffen --- CODEOWNERS | 1 + esphome/components/ili9341/display.py | 154 +------ esphome/components/ili9341/ili9341_defines.h | 83 ---- .../components/ili9341/ili9341_display.cpp | 308 ------------- esphome/components/ili9341/ili9341_init.h | 70 --- esphome/components/ili9xxx/__init__.py | 0 esphome/components/ili9xxx/display.py | 159 +++++++ esphome/components/ili9xxx/ili9xxx_defines.h | 96 ++++ .../components/ili9xxx/ili9xxx_display.cpp | 416 ++++++++++++++++++ .../ili9xxx_display.h} | 118 ++--- esphome/components/ili9xxx/ili9xxx_init.h | 174 ++++++++ tests/test1.yaml | 10 +- 12 files changed, 917 insertions(+), 672 deletions(-) delete mode 100644 esphome/components/ili9341/ili9341_defines.h delete mode 100644 esphome/components/ili9341/ili9341_display.cpp delete mode 100644 esphome/components/ili9341/ili9341_init.h create mode 100644 esphome/components/ili9xxx/__init__.py create mode 100644 esphome/components/ili9xxx/display.py create mode 100644 esphome/components/ili9xxx/ili9xxx_defines.h create mode 100644 esphome/components/ili9xxx/ili9xxx_display.cpp rename esphome/components/{ili9341/ili9341_display.h => ili9xxx/ili9xxx_display.h} (51%) create mode 100644 esphome/components/ili9xxx/ili9xxx_init.h diff --git a/CODEOWNERS b/CODEOWNERS index 3d6ea5cd32..c006db2a6a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -111,6 +111,7 @@ esphome/components/hte501/* @Stock-M esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core esphome/components/i2s_audio/* @jesserockz +esphome/components/ili9xxx/* @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 0b87a0c4eb..6ab6a5e8ef 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -1,153 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome import core, pins -from esphome.components import display, spi -from esphome.const import ( - CONF_COLOR_PALETTE, - CONF_DC_PIN, - CONF_ID, - CONF_LAMBDA, - CONF_MODEL, - CONF_PAGES, - CONF_RAW_DATA_ID, - CONF_RESET_PIN, + +CONFIG_SCHEMA = cv.invalid( + "The ili9341 platform component has been renamed to ili9xxx." ) -from esphome.core import CORE, HexInt - -DEPENDENCIES = ["spi"] - -CONF_COLOR_PALETTE_IMAGES = "color_palette_images" -CONF_LED_PIN = "led_pin" - -ili9341_ns = cg.esphome_ns.namespace("ili9341") -ili9341 = ili9341_ns.class_( - "ILI9341Display", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer -) -ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341) -ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341) -ILI9341TFT24R = ili9341_ns.class_("ILI9341TFT24R", ili9341) - -ILI9341Model = ili9341_ns.enum("ILI9341Model") -ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode") - -MODELS = { - "M5STACK": ILI9341Model.M5STACK, - "TFT_2.4": ILI9341Model.TFT_24, - "TFT_2.4R": ILI9341Model.TFT_24R, -} - -ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_") - -COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") - - -def _validate(config): - if config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and not config.get( - CONF_COLOR_PALETTE_IMAGES - ): - raise cv.Invalid( - "Color palette in IMAGE_ADAPTIVE mode requires at least one 'color_palette_images' entry to generate palette" - ) - if ( - config.get(CONF_COLOR_PALETTE_IMAGES) - and config.get(CONF_COLOR_PALETTE) != "IMAGE_ADAPTIVE" - ): - raise cv.Invalid( - "Providing color palette images requires palette mode to be 'IMAGE_ADAPTIVE'" - ) - return config - - -CONFIG_SCHEMA = cv.All( - display.FULL_DISPLAY_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(ili9341), - cv.Required(CONF_MODEL): ILI9341_MODEL, - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE, - cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( - cv.file_ - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - } - ) - .extend(cv.polling_component_schema("1s")) - .extend(spi.spi_device_schema(False)), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), - _validate, -) - - -async def to_code(config): - if config[CONF_MODEL] == "M5STACK": - lcd_type = ILI9341M5Stack - if config[CONF_MODEL] == "TFT_2.4": - lcd_type = ILI9341TFT24 - if config[CONF_MODEL] == "TFT_2.4R": - lcd_type = ILI9341TFT24R - rhs = lcd_type.new() - var = cg.Pvariable(config[CONF_ID], rhs) - - await cg.register_component(var, config) - await display.register_display(var, config) - await spi.register_spi_device(var, config) - cg.add(var.set_model(config[CONF_MODEL])) - dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) - cg.add(var.set_dc_pin(dc)) - - if CONF_LAMBDA in config: - lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void - ) - cg.add(var.set_writer(lambda_)) - if CONF_RESET_PIN in config: - reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) - cg.add(var.set_reset_pin(reset)) - if CONF_LED_PIN in config: - led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN]) - cg.add(var.set_led_pin(led_pin)) - - rhs = None - if config[CONF_COLOR_PALETTE] == "GRAYSCALE": - cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED)) - rhs = [] - for x in range(256): - rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) - elif config[CONF_COLOR_PALETTE] == "IMAGE_ADAPTIVE": - cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED)) - from PIL import Image - - def load_image(filename): - path = CORE.relative_config_path(filename) - try: - return Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") - - # make a wide horizontal combined image. - images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]] - total_width = sum(i.width for i in images) - max_height = max(i.height for i in images) - - ref_image = Image.new("RGB", (total_width, max_height)) - x = 0 - for i in images: - ref_image.paste(i, (x, 0)) - x = x + i.width - - # reduce the colors on combined image to 256. - converted = ref_image.convert("P", palette=Image.ADAPTIVE, colors=256) - # if you want to verify how the images look use - # ref_image.save("ref_in.png") - # converted.save("ref_out.png") - palette = converted.getpalette() - assert len(palette) == 256 * 3 - rhs = palette - else: - cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8)) - - if rhs is not None: - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.add(var.set_palette(prog_arr)) diff --git a/esphome/components/ili9341/ili9341_defines.h b/esphome/components/ili9341/ili9341_defines.h deleted file mode 100644 index 6b3d4c0dcf..0000000000 --- a/esphome/components/ili9341/ili9341_defines.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -namespace esphome { -namespace ili9341 { - -// Color definitions -// clang-format off -static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top -static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left -static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode -static const uint8_t MADCTL_ML = 0x10; ///< Bit 4 LCD refresh Bottom to top -static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order -static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order -static const uint8_t MADCTL_MH = 0x04; ///< Bit 2 LCD refresh right to left -// clang-format on - -static const uint16_t ILI9341_TFTWIDTH = 320; ///< ILI9341 max TFT width -static const uint16_t ILI9341_TFTHEIGHT = 240; ///< ILI9341 max TFT height - -// All ILI9341 specific commands some are used by init() -static const uint8_t ILI9341_NOP = 0x00; -static const uint8_t ILI9341_SWRESET = 0x01; -static const uint8_t ILI9341_RDDID = 0x04; -static const uint8_t ILI9341_RDDST = 0x09; - -static const uint8_t ILI9341_SLPIN = 0x10; -static const uint8_t ILI9341_SLPOUT = 0x11; -static const uint8_t ILI9341_PTLON = 0x12; -static const uint8_t ILI9341_NORON = 0x13; - -static const uint8_t ILI9341_RDMODE = 0x0A; -static const uint8_t ILI9341_RDMADCTL = 0x0B; -static const uint8_t ILI9341_RDPIXFMT = 0x0C; -static const uint8_t ILI9341_RDIMGFMT = 0x0A; -static const uint8_t ILI9341_RDSELFDIAG = 0x0F; - -static const uint8_t ILI9341_INVOFF = 0x20; -static const uint8_t ILI9341_INVON = 0x21; -static const uint8_t ILI9341_GAMMASET = 0x26; -static const uint8_t ILI9341_DISPOFF = 0x28; -static const uint8_t ILI9341_DISPON = 0x29; - -static const uint8_t ILI9341_CASET = 0x2A; -static const uint8_t ILI9341_PASET = 0x2B; -static const uint8_t ILI9341_RAMWR = 0x2C; -static const uint8_t ILI9341_RAMRD = 0x2E; - -static const uint8_t ILI9341_PTLAR = 0x30; -static const uint8_t ILI9341_VSCRDEF = 0x33; -static const uint8_t ILI9341_MADCTL = 0x36; -static const uint8_t ILI9341_VSCRSADD = 0x37; -static const uint8_t ILI9341_PIXFMT = 0x3A; - -static const uint8_t ILI9341_WRDISBV = 0x51; -static const uint8_t ILI9341_RDDISBV = 0x52; -static const uint8_t ILI9341_WRCTRLD = 0x53; - -static const uint8_t ILI9341_FRMCTR1 = 0xB1; -static const uint8_t ILI9341_FRMCTR2 = 0xB2; -static const uint8_t ILI9341_FRMCTR3 = 0xB3; -static const uint8_t ILI9341_INVCTR = 0xB4; -static const uint8_t ILI9341_DFUNCTR = 0xB6; - -static const uint8_t ILI9341_PWCTR1 = 0xC0; -static const uint8_t ILI9341_PWCTR2 = 0xC1; -static const uint8_t ILI9341_PWCTR3 = 0xC2; -static const uint8_t ILI9341_PWCTR4 = 0xC3; -static const uint8_t ILI9341_PWCTR5 = 0xC4; -static const uint8_t ILI9341_VMCTR1 = 0xC5; -static const uint8_t ILI9341_VMCTR2 = 0xC7; - -static const uint8_t ILI9341_RDID4 = 0xD3; -static const uint8_t ILI9341_RDINDEX = 0xD9; -static const uint8_t ILI9341_RDID1 = 0xDA; -static const uint8_t ILI9341_RDID2 = 0xDB; -static const uint8_t ILI9341_RDID3 = 0xDC; -static const uint8_t ILI9341_RDIDX = 0xDD; // TBC - -static const uint8_t ILI9341_GMCTRP1 = 0xE0; -static const uint8_t ILI9341_GMCTRN1 = 0xE1; - -} // namespace ili9341 -} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp deleted file mode 100644 index 9f9edcf21f..0000000000 --- a/esphome/components/ili9341/ili9341_display.cpp +++ /dev/null @@ -1,308 +0,0 @@ -#include "ili9341_display.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace ili9341 { - -static const char *const TAG = "ili9341"; - -void ILI9341Display::setup_pins_() { - this->dc_pin_->setup(); // OUTPUT - this->dc_pin_->digital_write(false); - if (this->reset_pin_ != nullptr) { - this->reset_pin_->setup(); // OUTPUT - this->reset_pin_->digital_write(true); - } - if (this->led_pin_ != nullptr) { - this->led_pin_->setup(); - this->led_pin_->digital_write(true); - } - this->spi_setup(); - - this->reset_(); -} - -void ILI9341Display::dump_config() { - LOG_DISPLAY("", "ili9341", this); - LOG_PIN(" Reset Pin: ", this->reset_pin_); - LOG_PIN(" DC Pin: ", this->dc_pin_); - LOG_PIN(" Busy Pin: ", this->busy_pin_); - LOG_UPDATE_INTERVAL(this); -} - -float ILI9341Display::get_setup_priority() const { return setup_priority::HARDWARE; } - -void ILI9341Display::command(uint8_t value) { - this->start_command_(); - this->write_byte(value); - this->end_command_(); -} - -void ILI9341Display::reset_() { - if (this->reset_pin_ != nullptr) { - this->reset_pin_->digital_write(false); - delay(10); - this->reset_pin_->digital_write(true); - delay(10); - } -} - -void ILI9341Display::data(uint8_t value) { - this->start_data_(); - this->write_byte(value); - this->end_data_(); -} - -void ILI9341Display::send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes) { - this->command(command_byte); // Send the command byte - this->start_data_(); - this->write_array(data_bytes, num_data_bytes); - this->end_data_(); -} - -uint8_t ILI9341Display::read_command(uint8_t command_byte, uint8_t index) { - uint8_t data = 0x10 + index; - this->send_command(0xD9, &data, 1); // Set Index Register - uint8_t result; - this->start_command_(); - this->write_byte(command_byte); - this->start_data_(); - do { - result = this->read_byte(); - } while (index--); - this->end_data_(); - return result; -} - -void ILI9341Display::update() { - this->do_update_(); - this->display_(); -} - -void ILI9341Display::display_() { - // we will only update the changed window to the display - uint16_t w = this->x_high_ - this->x_low_ + 1; - uint16_t h = this->y_high_ - this->y_low_ + 1; - uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); - - // check if something was displayed - if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { - return; - } - - set_addr_window_(this->x_low_, this->y_low_, w, h); - - ESP_LOGVV("ILI9341", "Start ILI9341Display::display_(xl:%d, xh:%d, yl:%d, yh:%d, w:%d, h:%d, start_pos:%d)", - this->x_low_, this->x_high_, this->y_low_, this->y_high_, w, h, start_pos); - - this->start_data_(); - for (uint16_t row = 0; row < h; row++) { - uint32_t pos = start_pos + (row * width_); - uint32_t rem = w; - - while (rem > 0) { - uint32_t sz = buffer_to_transfer_(pos, rem); - this->write_array(transfer_buffer_, 2 * sz); - pos += sz; - rem -= sz; - App.feed_wdt(); - } - App.feed_wdt(); - } - this->end_data_(); - - // invalidate watermarks - this->x_low_ = this->width_; - this->y_low_ = this->height_; - this->x_high_ = 0; - this->y_high_ = 0; -} - -void ILI9341Display::fill(Color color) { - uint8_t color332 = 0; - if (this->buffer_color_mode_ == BITS_8) { - color332 = display::ColorUtil::color_to_332(color); - } else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) - color332 = display::ColorUtil::color_to_index8_palette888(color, this->palette_); - } - memset(this->buffer_, color332, this->get_buffer_length_()); - this->x_low_ = 0; - this->y_low_ = 0; - this->x_high_ = this->get_width_internal() - 1; - this->y_high_ = this->get_height_internal() - 1; -} - -void ILI9341Display::fill_internal_(uint8_t color) { - memset(transfer_buffer_, color, sizeof(transfer_buffer_)); - - uint32_t rem = (this->get_buffer_length_() * 2); - - this->set_addr_window_(0, 0, this->get_width_internal(), this->get_height_internal()); - this->start_data_(); - - while (rem > 0) { - size_t sz = rem <= sizeof(transfer_buffer_) ? rem : sizeof(transfer_buffer_); - this->write_array(transfer_buffer_, sz); - rem -= sz; - } - - this->end_data_(); - - memset(buffer_, color, this->get_buffer_length_()); -} - -void ILI9341Display::rotate_my_(uint8_t m) { - uint8_t rotation = m & 3; // can't be higher than 3 - switch (rotation) { - case 0: - m = (MADCTL_MX | MADCTL_BGR); - // _width = ILI9341_TFTWIDTH; - // _height = ILI9341_TFTHEIGHT; - break; - case 1: - m = (MADCTL_MV | MADCTL_BGR); - // _width = ILI9341_TFTHEIGHT; - // _height = ILI9341_TFTWIDTH; - break; - case 2: - m = (MADCTL_MY | MADCTL_BGR); - // _width = ILI9341_TFTWIDTH; - // _height = ILI9341_TFTHEIGHT; - break; - case 3: - m = (MADCTL_MX | MADCTL_MY | MADCTL_MV | MADCTL_BGR); - // _width = ILI9341_TFTHEIGHT; - // _height = ILI9341_TFTWIDTH; - break; - } - - this->command(ILI9341_MADCTL); - this->data(m); -} - -void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) - return; - - uint32_t pos = (y * width_) + x; - uint8_t new_color; - - if (this->buffer_color_mode_ == BITS_8) { - new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); - } else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) { - new_color = display::ColorUtil::color_to_index8_palette888(color, this->palette_); - } - - if (buffer_[pos] != new_color) { - buffer_[pos] = new_color; - // low and high watermark may speed up drawing from buffer - this->x_low_ = (x < this->x_low_) ? x : this->x_low_; - this->y_low_ = (y < this->y_low_) ? y : this->y_low_; - this->x_high_ = (x > this->x_high_) ? x : this->x_high_; - this->y_high_ = (y > this->y_high_) ? y : this->y_high_; - } -} - -// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color -// values per bit is huge -uint32_t ILI9341Display::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } - -void ILI9341Display::start_command_() { - this->dc_pin_->digital_write(false); - this->enable(); -} - -void ILI9341Display::end_command_() { this->disable(); } -void ILI9341Display::start_data_() { - this->dc_pin_->digital_write(true); - this->enable(); -} -void ILI9341Display::end_data_() { this->disable(); } - -void ILI9341Display::init_lcd_(const uint8_t *init_cmd) { - uint8_t cmd, x, num_args; - const uint8_t *addr = init_cmd; - while ((cmd = progmem_read_byte(addr++)) > 0) { - x = progmem_read_byte(addr++); - num_args = x & 0x7F; - send_command(cmd, addr, num_args); - addr += num_args; - if (x & 0x80) - delay(150); // NOLINT - } -} - -void ILI9341Display::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { - uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); - this->command(ILI9341_CASET); // Column address set - this->start_data_(); - this->write_byte(x1 >> 8); - this->write_byte(x1); - this->write_byte(x2 >> 8); - this->write_byte(x2); - this->end_data_(); - this->command(ILI9341_PASET); // Row address set - this->start_data_(); - this->write_byte(y1 >> 8); - this->write_byte(y1); - this->write_byte(y2 >> 8); - this->write_byte(y2); - this->end_data_(); - this->command(ILI9341_RAMWR); // Write to RAM -} - -void ILI9341Display::invert_display_(bool invert) { this->command(invert ? ILI9341_INVON : ILI9341_INVOFF); } - -int ILI9341Display::get_width_internal() { return this->width_; } -int ILI9341Display::get_height_internal() { return this->height_; } - -uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) { - uint8_t *src = buffer_ + pos; - uint8_t *dst = transfer_buffer_; - - if (sz > sizeof(transfer_buffer_) / 2) { - sz = sizeof(transfer_buffer_) / 2; - } - - for (uint32_t i = 0; i < sz; ++i) { - uint16_t color; - if (this->buffer_color_mode_ == BITS_8) { - color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); - } else { // if (this->buffer_color_mode == BITS_8_INDEXED) { - Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_); - color = display::ColorUtil::color_to_565(col); - } - *dst++ = (uint8_t)(color >> 8); - *dst++ = (uint8_t) color; - } - - return sz; -} - -// M5Stack display -void ILI9341M5Stack::initialize() { - this->init_lcd_(INITCMD_M5STACK); - this->width_ = 320; - this->height_ = 240; - this->invert_display_(true); -} - -// 24_TFT display -void ILI9341TFT24::initialize() { - this->init_lcd_(INITCMD_TFT); - this->width_ = 240; - this->height_ = 320; -} - -// 24_TFT rotated display -void ILI9341TFT24R::initialize() { - this->init_lcd_(INITCMD_TFT); - this->width_ = 320; - this->height_ = 240; -} - -} // namespace ili9341 -} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_init.h b/esphome/components/ili9341/ili9341_init.h deleted file mode 100644 index b4f67ff19a..0000000000 --- a/esphome/components/ili9341/ili9341_init.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once -#include "esphome/core/helpers.h" - -namespace esphome { -namespace ili9341 { - -// clang-format off -static const uint8_t PROGMEM INITCMD_M5STACK[] = { - 0xEF, 3, 0x03, 0x80, 0x02, - 0xCF, 3, 0x00, 0xC1, 0x30, - 0xED, 4, 0x64, 0x03, 0x12, 0x81, - 0xE8, 3, 0x85, 0x00, 0x78, - 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, - 0xF7, 1, 0x20, - 0xEA, 2, 0x00, 0x00, - ILI9341_PWCTR1 , 1, 0x23, // Power control VRH[5:0] - ILI9341_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] - ILI9341_VMCTR1 , 2, 0x3e, 0x28, // VCM control - ILI9341_VMCTR2 , 1, 0x86, // VCM control2 - ILI9341_MADCTL , 1, MADCTL_BGR, // Memory Access Control - ILI9341_VSCRSADD, 1, 0x00, // Vertical scroll zero - ILI9341_PIXFMT , 1, 0x55, - ILI9341_FRMCTR1 , 2, 0x00, 0x13, - ILI9341_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control - 0xF2, 1, 0x00, // 3Gamma Function Disable - ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected - ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma - 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, - 0x0E, 0x09, 0x00, - ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma - 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, - 0x31, 0x36, 0x0F, - ILI9341_SLPOUT , 0x80, // Exit Sleep - ILI9341_DISPON , 0x80, // Display on - 0x00 // End of list -}; - -static const uint8_t PROGMEM INITCMD_TFT[] = { - 0xEF, 3, 0x03, 0x80, 0x02, - 0xCF, 3, 0x00, 0xC1, 0x30, - 0xED, 4, 0x64, 0x03, 0x12, 0x81, - 0xE8, 3, 0x85, 0x00, 0x78, - 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, - 0xF7, 1, 0x20, - 0xEA, 2, 0x00, 0x00, - ILI9341_PWCTR1 , 1, 0x23, // Power control VRH[5:0] - ILI9341_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] - ILI9341_VMCTR1 , 2, 0x3e, 0x28, // VCM control - ILI9341_VMCTR2 , 1, 0x86, // VCM control2 - ILI9341_MADCTL , 1, 0x48, // Memory Access Control - ILI9341_VSCRSADD, 1, 0x00, // Vertical scroll zero - ILI9341_PIXFMT , 1, 0x55, - ILI9341_FRMCTR1 , 2, 0x00, 0x18, - ILI9341_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control - 0xF2, 1, 0x00, // 3Gamma Function Disable - ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected - ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma - 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, - 0x0E, 0x09, 0x00, - ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma - 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, - 0x31, 0x36, 0x0F, - ILI9341_SLPOUT , 0x80, // Exit Sleep - ILI9341_DISPON , 0x80, // Display on - 0x00 // End of list -}; - -// clang-format on -} // namespace ili9341 -} // namespace esphome diff --git a/esphome/components/ili9xxx/__init__.py b/esphome/components/ili9xxx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py new file mode 100644 index 0000000000..437fc93b89 --- /dev/null +++ b/esphome/components/ili9xxx/display.py @@ -0,0 +1,159 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import core, pins +from esphome.components import display, spi +from esphome.core import CORE, HexInt +from esphome.const import ( + CONF_COLOR_PALETTE, + CONF_DC_PIN, + CONF_ID, + CONF_LAMBDA, + CONF_MODEL, + CONF_RAW_DATA_ID, + CONF_PAGES, + CONF_RESET_PIN, + CONF_DIMENSIONS, +) + +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["psram"] + +CODEOWNERS = ["@nielsnl68"] + +ili9XXX_ns = cg.esphome_ns.namespace("ili9xxx") +ili9XXXSPI = ili9XXX_ns.class_( + "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer +) + +ILI9XXXColorMode = ili9XXX_ns.enum("ILI9XXXColorMode") + +MODELS = { + "M5STACK": ili9XXX_ns.class_("ILI9XXXM5Stack", ili9XXXSPI), + "M5CORE": ili9XXX_ns.class_("ILI9XXXM5CORE", ili9XXXSPI), + "TFT_2.4": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), + "TFT_2.4R": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), + "ILI9341": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), + "ILI9342": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), + "ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI), + "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), + "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), + "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), +} + +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") + +CONF_LED_PIN = "led_pin" +CONF_COLOR_PALETTE_IMAGES = "color_palette_images" + + +def _validate(config): + if config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and not config.get( + CONF_COLOR_PALETTE_IMAGES + ): + raise cv.Invalid( + "Color palette in IMAGE_ADAPTIVE mode requires at least one 'color_palette_images' entry to generate palette" + ) + if ( + config.get(CONF_COLOR_PALETTE_IMAGES) + and config.get(CONF_COLOR_PALETTE) != "IMAGE_ADAPTIVE" + ): + raise cv.Invalid( + "Providing color palette images requires palette mode to be 'IMAGE_ADAPTIVE'" + ) + return config + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ili9XXXSPI), + cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True, space="_"), + cv.Optional(CONF_DIMENSIONS): cv.dimensions, + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_LED_PIN): cv.invalid( + "This property is removed. To use the backlight use proper light component." + ), + cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( + cv.file_ + ), + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(spi.spi_device_schema(False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, +) + + +async def to_code(config): + rhs = MODELS[config[CONF_MODEL]].new() + var = cg.Pvariable(config[CONF_ID], rhs) + + await cg.register_component(var, config) + await display.register_display(var, config) + await spi.register_spi_device(var, config) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + if CONF_RESET_PIN in config: + reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + if CONF_DIMENSIONS in config: + cg.add( + var.set_dimentions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]) + ) + + rhs = None + if config[CONF_COLOR_PALETTE] == "GRAYSCALE": + cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8_INDEXED)) + rhs = [] + for x in range(256): + rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_palette(prog_arr)) + elif config[CONF_COLOR_PALETTE] == "IMAGE_ADAPTIVE": + cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8_INDEXED)) + from PIL import Image + + def load_image(filename): + path = CORE.relative_config_path(filename) + try: + return Image.open(path) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") + + # make a wide horizontal combined image. + images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]] + total_width = sum(i.width for i in images) + max_height = max(i.height for i in images) + + ref_image = Image.new("RGB", (total_width, max_height)) + x = 0 + for i in images: + ref_image.paste(i, (x, 0)) + x = x + i.width + + # reduce the colors on combined image to 256. + converted = ref_image.convert("P", palette=Image.ADAPTIVE, colors=256) + # if you want to verify how the images look use + # ref_image.save("ref_in.png") + # converted.save("ref_out.png") + palette = converted.getpalette() + assert len(palette) == 256 * 3 + rhs = palette + else: + cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16)) + + if rhs is not None: + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_palette(prog_arr)) diff --git a/esphome/components/ili9xxx/ili9xxx_defines.h b/esphome/components/ili9xxx/ili9xxx_defines.h new file mode 100644 index 0000000000..29483ee15e --- /dev/null +++ b/esphome/components/ili9xxx/ili9xxx_defines.h @@ -0,0 +1,96 @@ +#pragma once + +namespace esphome { +namespace ili9xxx { + +// Color definitions +// clang-format off +static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top +static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left +static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode +static const uint8_t MADCTL_ML = 0x10; ///< Bit 4 LCD refresh Bottom to top +static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order +static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order +static const uint8_t MADCTL_MH = 0x04; ///< Bit 2 LCD refresh right to left +// clang-format on + +// All ILI9XXX specific commands some are used by init() +static const uint8_t ILI9XXX_NOP = 0x00; +static const uint8_t ILI9XXX_SWRESET = 0x01; +static const uint8_t ILI9XXX_RDDID = 0x04; +static const uint8_t ILI9XXX_RDDST = 0x09; + +static const uint8_t ILI9XXX_SLPIN = 0x10; +static const uint8_t ILI9XXX_SLPOUT = 0x11; +static const uint8_t ILI9XXX_PTLON = 0x12; +static const uint8_t ILI9XXX_NORON = 0x13; + +static const uint8_t ILI9XXX_RDMODE = 0x0A; +static const uint8_t ILI9XXX_RDMADCTL = 0x0B; +static const uint8_t ILI9XXX_RDPIXFMT = 0x0C; +static const uint8_t ILI9XXX_RDIMGFMT = 0x0D; +static const uint8_t ILI9XXX_RDSELFDIAG = 0x0F; + +static const uint8_t ILI9XXX_INVOFF = 0x20; +static const uint8_t ILI9XXX_INVON = 0x21; +static const uint8_t ILI9XXX_GAMMASET = 0x26; +static const uint8_t ILI9XXX_DISPOFF = 0x28; +static const uint8_t ILI9XXX_DISPON = 0x29; + +static const uint8_t ILI9XXX_CASET = 0x2A; +static const uint8_t ILI9XXX_PASET = 0x2B; +static const uint8_t ILI9XXX_RAMWR = 0x2C; +static const uint8_t ILI9XXX_RAMRD = 0x2E; + +static const uint8_t ILI9XXX_PTLAR = 0x30; +static const uint8_t ILI9XXX_VSCRDEF = 0x33; +static const uint8_t ILI9XXX_MADCTL = 0x36; +static const uint8_t ILI9XXX_VSCRSADD = 0x37; +static const uint8_t ILI9XXX_IDMOFF = 0x38; +static const uint8_t ILI9XXX_IDMON = 0x39; +static const uint8_t ILI9XXX_PIXFMT = 0x3A; +static const uint8_t ILI9XXX_COLMOD = 0x3A; + +static const uint8_t ILI9XXX_GETSCANLINE = 0x45; + +static const uint8_t ILI9XXX_WRDISBV = 0x51; +static const uint8_t ILI9XXX_RDDISBV = 0x52; +static const uint8_t ILI9XXX_WRCTRLD = 0x53; + +static const uint8_t ILI9XXX_IFMODE = 0xB0; +static const uint8_t ILI9XXX_FRMCTR1 = 0xB1; +static const uint8_t ILI9XXX_FRMCTR2 = 0xB2; +static const uint8_t ILI9XXX_FRMCTR3 = 0xB3; +static const uint8_t ILI9XXX_INVCTR = 0xB4; +static const uint8_t ILI9XXX_DFUNCTR = 0xB6; +static const uint8_t ILI9XXX_ETMOD = 0xB7; + +static const uint8_t ILI9XXX_PWCTR1 = 0xC0; +static const uint8_t ILI9XXX_PWCTR2 = 0xC1; +static const uint8_t ILI9XXX_PWCTR3 = 0xC2; +static const uint8_t ILI9XXX_PWCTR4 = 0xC3; +static const uint8_t ILI9XXX_PWCTR5 = 0xC4; +static const uint8_t ILI9XXX_VMCTR1 = 0xC5; +static const uint8_t ILI9XXX_IFCTR = 0xC6; +static const uint8_t ILI9XXX_VMCTR2 = 0xC7; +static const uint8_t ILI9XXX_GMCTR = 0xC8; +static const uint8_t ILI9XXX_SETEXTC = 0xC8; + +static const uint8_t ILI9XXX_PWSET = 0xD0; +static const uint8_t ILI9XXX_VMCTR = 0xD1; +static const uint8_t ILI9XXX_PWSETN = 0xD2; +static const uint8_t ILI9XXX_RDID4 = 0xD3; +static const uint8_t ILI9XXX_RDINDEX = 0xD9; +static const uint8_t ILI9XXX_RDID1 = 0xDA; +static const uint8_t ILI9XXX_RDID2 = 0xDB; +static const uint8_t ILI9XXX_RDID3 = 0xDC; +static const uint8_t ILI9XXX_RDIDX = 0xDD; // TBC + +static const uint8_t ILI9XXX_GMCTRP1 = 0xE0; +static const uint8_t ILI9XXX_GMCTRN1 = 0xE1; + +static const uint8_t ILI9XXX_CSCON = 0xF0; +static const uint8_t ILI9XXX_ADJCTL3 = 0xF7; + +} // namespace ili9xxx +} // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp new file mode 100644 index 0000000000..0091a2aabc --- /dev/null +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -0,0 +1,416 @@ +#include "ili9xxx_display.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ili9xxx { + +static const char *const TAG = "ili9xxx"; + +void ILI9XXXDisplay::setup() { + this->setup_pins_(); + this->initialize(); + + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; + if (this->buffer_color_mode_ == BITS_16) { + this->init_internal_(this->get_buffer_length_() * 2); + if (this->buffer_ != nullptr) { + return; + } + this->buffer_color_mode_ = BITS_8; + } + this->init_internal_(this->get_buffer_length_()); + if (this->buffer_ == nullptr) { + this->mark_failed(); + } +} + +void ILI9XXXDisplay::setup_pins_() { + this->dc_pin_->setup(); // OUTPUT + this->dc_pin_->digital_write(false); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); // OUTPUT + this->reset_pin_->digital_write(true); + } + + this->spi_setup(); + + this->reset_(); +} + +void ILI9XXXDisplay::dump_config() { + LOG_DISPLAY("", "ili9xxx", this); + switch (this->buffer_color_mode_) { + case BITS_8_INDEXED: + ESP_LOGCONFIG(TAG, " Color mode: 8bit Indexed"); + break; + case BITS_16: + ESP_LOGCONFIG(TAG, " Color mode: 16bit"); + break; + default: + ESP_LOGCONFIG(TAG, " Color mode: 8bit 332 mode"); + break; + } + if (this->is_18bitdisplay_) { + ESP_LOGCONFIG(TAG, " 18-Bit Mode: YES"); + } + + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + + if (this->is_failed()) { + ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); + } + LOG_UPDATE_INTERVAL(this); +} + +float ILI9XXXDisplay::get_setup_priority() const { return setup_priority::HARDWARE; } + +void ILI9XXXDisplay::fill(Color color) { + uint16_t new_color = 0; + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->get_width_internal() - 1; + this->y_high_ = this->get_height_internal() - 1; + switch (this->buffer_color_mode_) { + case BITS_8_INDEXED: + new_color = display::ColorUtil::color_to_index8_palette888(color, this->palette_); + break; + case BITS_16: + new_color = display::ColorUtil::color_to_565(color); + for (uint32_t i = 0; i < this->get_buffer_length_() * 2; i = i + 2) { + this->buffer_[i] = (uint8_t)(new_color >> 8); + this->buffer_[i + 1] = (uint8_t) new_color; + } + return; + break; + default: + new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + break; + } + memset(this->buffer_, (uint8_t) new_color, this->get_buffer_length_()); +} + +void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + uint32_t pos = (y * width_) + x; + uint16_t new_color; + bool updated = false; + switch (this->buffer_color_mode_) { + case BITS_8_INDEXED: + new_color = display::ColorUtil::color_to_index8_palette888(color, this->palette_); + break; + case BITS_16: + pos = pos * 2; + new_color = display::ColorUtil::color_to_565(color, display::ColorOrder::COLOR_ORDER_RGB); + if (this->buffer_[pos] != (uint8_t)(new_color >> 8)) { + this->buffer_[pos] = (uint8_t)(new_color >> 8); + updated = true; + } + pos = pos + 1; + new_color = new_color & 0xFF; + break; + default: + new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + break; + } + + if (this->buffer_[pos] != new_color) { + this->buffer_[pos] = new_color; + updated = true; + } + if (updated) { + // low and high watermark may speed up drawing from buffer + this->x_low_ = (x < this->x_low_) ? x : this->x_low_; + this->y_low_ = (y < this->y_low_) ? y : this->y_low_; + this->x_high_ = (x > this->x_high_) ? x : this->x_high_; + this->y_high_ = (y > this->y_high_) ? y : this->y_high_; + // ESP_LOGVV(TAG, "=>>> pixel (x:%d, y:%d) (xl:%d, xh:%d, yl:%d, yh:%d", x, y, this->x_low_, this->x_high_, + // this->y_low_, this->y_high_); + } +} + +void ILI9XXXDisplay::update() { + if (this->prossing_update_) { + this->need_update_ = true; + return; + } + do { + this->prossing_update_ = true; + this->need_update_ = false; + if (!this->need_update_) { + this->do_update_(); + } + } while (this->need_update_); + this->prossing_update_ = false; + this->display_(); +} + +void ILI9XXXDisplay::display_() { + // we will only update the changed window to the display + uint16_t w = this->x_high_ - this->x_low_ + 1; // NOLINT + uint16_t h = this->y_high_ - this->y_low_ + 1; // NOLINT + uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); + + // check if something was displayed + if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { + ESP_LOGV(TAG, "Nothing to display"); + return; + } + + set_addr_window_(this->x_low_, this->y_low_, w, h); + + ESP_LOGV(TAG, + "Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, " + "heigth:%d, start_pos:%d)", + this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, start_pos); + + this->start_data_(); + for (uint16_t row = 0; row < h; row++) { + uint32_t pos = start_pos + (row * width_); + uint32_t rem = w; + + while (rem > 0) { + uint32_t sz = std::min(rem, ILI9XXX_TRANSFER_BUFFER_SIZE); + // ESP_LOGVV(TAG, "Send to display(pos:%d, rem:%d, zs:%d)", pos, rem, sz); + buffer_to_transfer_(pos, sz); + if (this->is_18bitdisplay_) { + for (uint32_t i = 0; i < sz; ++i) { + uint16_t color_val = transfer_buffer_[i]; + + uint8_t red = color_val & 0x1F; + uint8_t green = (color_val & 0x7E0) >> 5; + uint8_t blue = (color_val & 0xF800) >> 11; + + uint8_t pass_buff[3]; + + pass_buff[2] = (uint8_t)((red / 32.0) * 64) << 2; + pass_buff[1] = (uint8_t) green << 2; + pass_buff[0] = (uint8_t)((blue / 32.0) * 64) << 2; + + this->write_array(pass_buff, sizeof(pass_buff)); + } + } else { + this->write_array16(transfer_buffer_, sz); + } + pos += sz; + rem -= sz; + } + App.feed_wdt(); + } + this->end_data_(); + + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +uint32_t ILI9XXXDisplay::buffer_to_transfer_(uint32_t pos, uint32_t sz) { + for (uint32_t i = 0; i < sz; ++i) { + switch (this->buffer_color_mode_) { + case BITS_8_INDEXED: + transfer_buffer_[i] = display::ColorUtil::color_to_565( + display::ColorUtil::index8_to_color_palette888(this->buffer_[pos + i], this->palette_)); + break; + case BITS_16: + transfer_buffer_[i] = ((uint16_t) this->buffer_[(pos + i) * 2] << 8) | this->buffer_[((pos + i) * 2) + 1]; + continue; + break; + default: + transfer_buffer_[i] = + display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos + i])); + break; + } + } + return sz; +} + +// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color +// values per bit is huge +uint32_t ILI9XXXDisplay::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } + +void ILI9XXXDisplay::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void ILI9XXXDisplay::data(uint8_t value) { + this->start_data_(); + this->write_byte(value); + this->end_data_(); +} + +void ILI9XXXDisplay::send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->command(command_byte); // Send the command byte + this->start_data_(); + this->write_array(data_bytes, num_data_bytes); + this->end_data_(); +} + +uint8_t ILI9XXXDisplay::read_command(uint8_t command_byte, uint8_t index) { + uint8_t data = 0x10 + index; + this->send_command(0xD9, &data, 1); // Set Index Register + uint8_t result; + this->start_command_(); + this->write_byte(command_byte); + this->start_data_(); + do { + result = this->read_byte(); + } while (index--); + this->end_data_(); + return result; +} + +void ILI9XXXDisplay::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} +void ILI9XXXDisplay::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} + +void ILI9XXXDisplay::end_command_() { this->disable(); } +void ILI9XXXDisplay::end_data_() { this->disable(); } + +void ILI9XXXDisplay::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) { + uint8_t cmd, x, num_args; + const uint8_t *addr = init_cmd; + while ((cmd = progmem_read_byte(addr++)) > 0) { + x = progmem_read_byte(addr++); + num_args = x & 0x7F; + send_command(cmd, addr, num_args); + addr += num_args; + if (x & 0x80) + delay(150); // NOLINT + } +} + +void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { + uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); + this->command(ILI9XXX_CASET); // Column address set + this->start_data_(); + this->write_byte(x1 >> 8); + this->write_byte(x1); + this->write_byte(x2 >> 8); + this->write_byte(x2); + this->end_data_(); + this->command(ILI9XXX_PASET); // Row address set + this->start_data_(); + this->write_byte(y1 >> 8); + this->write_byte(y1); + this->write_byte(y2 >> 8); + this->write_byte(y2); + this->end_data_(); + this->command(ILI9XXX_RAMWR); // Write to RAM +} + +void ILI9XXXDisplay::invert_display_(bool invert) { this->command(invert ? ILI9XXX_INVON : ILI9XXX_INVOFF); } + +int ILI9XXXDisplay::get_width_internal() { return this->width_; } +int ILI9XXXDisplay::get_height_internal() { return this->height_; } + +// M5Stack display +void ILI9XXXM5Stack::initialize() { + this->init_lcd_(INITCMD_M5STACK); + if (this->width_ == 0) + this->width_ = 320; + if (this->height_ == 0) + this->height_ = 240; + this->invert_display_(true); +} + +// M5CORE display // Based on the configuration settings of M5stact's M5GFX code. +void ILI9XXXM5CORE::initialize() { + this->init_lcd_(INITCMD_M5CORE); + if (this->width_ == 0) + this->width_ = 320; + if (this->height_ == 0) + this->height_ = 240; + this->invert_display_(true); +} + +// 24_TFT display +void ILI9XXXILI9341::initialize() { + this->init_lcd_(INITCMD_ILI9341); + if (this->width_ == 0) + this->width_ = 240; + if (this->height_ == 0) + this->height_ = 320; +} +// 24_TFT rotated display +void ILI9XXXILI9342::initialize() { + this->init_lcd_(INITCMD_ILI9341); + if (this->width_ == 0) { + this->width_ = 320; + } + if (this->height_ == 0) { + this->height_ = 240; + } +} + +// 35_TFT display +void ILI9XXXILI9481::initialize() { + this->init_lcd_(INITCMD_ILI9481); + if (this->width_ == 0) { + this->width_ = 480; + } + if (this->height_ == 0) { + this->height_ = 320; + } +} + +// 35_TFT display +void ILI9XXXILI9486::initialize() { + this->init_lcd_(INITCMD_ILI9486); + if (this->width_ == 0) { + this->width_ = 480; + } + if (this->height_ == 0) { + this->height_ = 320; + } +} +// 40_TFT display +void ILI9XXXILI9488::initialize() { + this->init_lcd_(INITCMD_ILI9488); + if (this->width_ == 0) { + this->width_ = 480; + } + if (this->height_ == 0) { + this->height_ = 320; + } + this->is_18bitdisplay_ = true; +} +// 40_TFT display +void ILI9XXXST7796::initialize() { + this->init_lcd_(INITCMD_ST7796); + if (this->width_ == 0) { + this->width_ = 320; + } + if (this->height_ == 0) { + this->height_ = 480; + } +} + +} // namespace ili9xxx +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9xxx/ili9xxx_display.h similarity index 51% rename from esphome/components/ili9341/ili9341_display.h rename to esphome/components/ili9xxx/ili9xxx_display.h index 547c608ae8..8a8cd4bb44 100644 --- a/esphome/components/ili9341/ili9341_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -1,27 +1,21 @@ #pragma once - -#include "esphome/core/component.h" #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" -#include "ili9341_defines.h" -#include "ili9341_init.h" -#include "esphome/core/log.h" +#include "ili9xxx_defines.h" +#include "ili9xxx_init.h" namespace esphome { -namespace ili9341 { +namespace ili9xxx { -enum ILI9341Model { - M5STACK = 0, - TFT_24, - TFT_24R, +const uint32_t ILI9XXX_TRANSFER_BUFFER_SIZE = 64; + +enum ILI9XXXColorMode { + BITS_8 = 0x08, + BITS_8_INDEXED = 0x09, + BITS_16 = 0x10, }; -enum ILI9341ColorMode { - BITS_8, - BITS_8_INDEXED, -}; - -class ILI9341Display : public PollingComponent, +class ILI9XXXDisplay : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevice { @@ -29,59 +23,46 @@ class ILI9341Display : public PollingComponent, void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } - void set_led_pin(GPIOPin *led) { this->led_pin_ = led; } - void set_model(ILI9341Model model) { this->model_ = model; } void set_palette(const uint8_t *palette) { this->palette_ = palette; } - void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; } - + void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; } + void set_dimentions(int16_t width, int16_t height) { + this->height_ = height; + this->width_ = width; + } void command(uint8_t value); void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); uint8_t read_command(uint8_t command_byte, uint8_t index); - virtual void initialize() = 0; void update() override; void fill(Color color) override; void dump_config() override; - void setup() override { - this->setup_pins_(); - this->initialize(); - - this->x_low_ = this->width_; - this->y_low_ = this->height_; - this->x_high_ = 0; - this->y_high_ = 0; - - this->init_internal_(this->get_buffer_length_()); - this->fill_internal_(0x00); - } + void setup() override; display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } protected: void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); + virtual void initialize() = 0; + void display_(); void init_lcd_(const uint8_t *init_cmd); void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); void invert_display_(bool invert); void reset_(); - void fill_internal_(uint8_t color); - void display_(); - void rotate_my_(uint8_t m); - ILI9341Model model_; - int16_t width_{320}; ///< Display width as modified by current rotation - int16_t height_{240}; ///< Display height as modified by current rotation + int16_t width_{0}; ///< Display width as modified by current rotation + int16_t height_{0}; ///< Display height as modified by current rotation uint16_t x_low_{0}; uint16_t y_low_{0}; uint16_t x_high_{0}; uint16_t y_high_{0}; const uint8_t *palette_; - ILI9341ColorMode buffer_color_mode_{BITS_8}; + ILI9XXXColorMode buffer_color_mode_{BITS_16}; uint32_t get_buffer_length_(); int get_width_internal() override; @@ -92,33 +73,66 @@ class ILI9341Display : public PollingComponent, void start_data_(); void end_data_(); - uint8_t transfer_buffer_[64]; + uint16_t transfer_buffer_[ILI9XXX_TRANSFER_BUFFER_SIZE]; uint32_t buffer_to_transfer_(uint32_t pos, uint32_t sz); GPIOPin *reset_pin_{nullptr}; - GPIOPin *led_pin_{nullptr}; - GPIOPin *dc_pin_; + GPIOPin *dc_pin_{nullptr}; GPIOPin *busy_pin_{nullptr}; + + bool prossing_update_ = false; + bool need_update_ = false; + bool is_18bitdisplay_ = false; }; //----------- M5Stack display -------------- -class ILI9341M5Stack : public ILI9341Display { - public: +class ILI9XXXM5Stack : public ILI9XXXDisplay { + protected: void initialize() override; }; -//----------- ILI9341_24_TFT display -------------- -class ILI9341TFT24 : public ILI9341Display { - public: +//----------- M5Stack display -------------- +class ILI9XXXM5CORE : public ILI9XXXDisplay { + protected: void initialize() override; }; -//----------- ILI9341_24_TFT rotated display -------------- -class ILI9341TFT24R : public ILI9341Display { - public: +//----------- ILI9XXX_24_TFT display -------------- +class ILI9XXXILI9341 : public ILI9XXXDisplay { + protected: void initialize() override; }; -} // namespace ili9341 +//----------- ILI9XXX_24_TFT rotated display -------------- +class ILI9XXXILI9342 : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + +//----------- ILI9XXX_??_TFT rotated display -------------- +class ILI9XXXILI9481 : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + +//----------- ILI9XXX_35_TFT rotated display -------------- +class ILI9XXXILI9486 : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + +//----------- ILI9XXX_35_TFT rotated display -------------- +class ILI9XXXILI9488 : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + +//----------- ILI9XXX_35_TFT rotated display -------------- +class ILI9XXXST7796 : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + +} // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h new file mode 100644 index 0000000000..e8d3614a1d --- /dev/null +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -0,0 +1,174 @@ +#pragma once +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ili9xxx { + +// clang-format off +static const uint8_t PROGMEM INITCMD_M5STACK[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9XXX_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9XXX_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9XXX_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9XXX_VMCTR2 , 1, 0x86, // VCM control2 + ILI9XXX_MADCTL , 1, MADCTL_BGR, // Memory Access Control + ILI9XXX_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR1 , 2, 0x00, 0x13, + ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_M5CORE[] = { + ILI9XXX_SETEXTC, 3, 0xFF,0x93,0x42, // Turn on the external command + ILI9XXX_PWCTR1 , 2, 0x12, 0x12, + ILI9XXX_PWCTR2 , 1, 0x03, + ILI9XXX_VMCTR1 , 1, 0xF2, + ILI9XXX_IFMODE , 1, 0xE0, + 0xF6 , 3, 0x01, 0x00, 0x00, + ILI9XXX_GMCTRP1,15, 0x00,0x0C,0x11,0x04,0x11,0x08,0x37,0x89,0x4C,0x06,0x0C,0x0A,0x2E,0x34,0x0F, + ILI9XXX_GMCTRN1,15, 0x00,0x0B,0x11,0x05,0x13,0x09,0x33,0x67,0x48,0x07,0x0E,0x0B,0x2E,0x33,0x0F, + ILI9XXX_DFUNCTR, 4, 0x08,0x82,0x1D,0x04, + ILI9XXX_IDMOFF , 0, + ILI9XXX_DISPON , 0x80, // Display on + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + + 0x00 // End of list +}; + + + +static const uint8_t PROGMEM INITCMD_ILI9341[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9XXX_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9XXX_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9XXX_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9XXX_VMCTR2 , 1, 0x86, // VCM control2 + ILI9XXX_MADCTL , 1, 0x48, // Memory Access Control + ILI9XXX_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR1 , 2, 0x00, 0x18, + ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_ILI9481[] = { + ILI9XXX_SLPOUT , 0x80, // Exit sleep mode + ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, + ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, + ILI9XXX_PWSETN , 2, 0x01, 0x11, + ILI9XXX_PWCTR1 , 5, 0x10, 0x3B, 0x00, 0x02, 0x11, + ILI9XXX_VMCTR1 , 1, 0x03, + ILI9XXX_IFCTR , 1, 0x83, + ILI9XXX_GMCTR ,12, 0x00, 0x26, 0x21, 0x00, 0x00, 0x1F, 0x65, 0x23, 0x77, 0x00, 0x0F, 0x00, + ILI9XXX_IFMODE , 1, 0x00, // CommandAccessProtect + 0xE4 , 1, 0xA0, + ILI9XXX_CSCON , 1, 0x01, + ILI9XXX_DISPON, 0x80, // Set display on + 0x00 // end +}; + +static const uint8_t PROGMEM INITCMD_ILI9486[] = { + ILI9XXX_SLPOUT, 0x80, + ILI9XXX_PIXFMT, 1, 0x55, + ILI9XXX_PWCTR3, 1, 0x44, + ILI9XXX_VMCTR1, 4, 0x00, 0x00, 0x00, 0x00, + ILI9XXX_GMCTRP1, 15, 0x0f,0x1f,0x1c,0x0c,0x0f,0x08,0x48,0x98,0x37,0x0a,0x13,0x04,0x11,0x0d,0x00, + ILI9XXX_GMCTRN1, 15, 0x0f,0x32,0x2e,0x0b,0x0d,0x05,0x47,0x75,0x37,0x06,0x10,0x03,0x24,0x20,0x00, + ILI9XXX_INVOFF, 0x80, + ILI9XXX_MADCTL, 1, 0x48, + ILI9XXX_DISPON, 0x80, + + // ILI9XXX_MADCTL, 1, MADCTL_BGR | MADCTL_MV, //hardware rotation + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_ILI9488[] = { + ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, + ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, + + ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2 + ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL + ILI9XXX_VMCTR1, 3, 0x00, 0x12, 0x80, // nVM VCM_REG VCM_REG_EN + + ILI9XXX_IFMODE, 1, 0x00, + ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz + ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot + + ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan + + 0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data + + ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3 + + ILI9XXX_MADCTL, 1, 0x28, + //ILI9XXX_PIXFMT, 1, 0x55, // Interface Pixel Format = 16bit + ILI9XXX_PIXFMT, 1, 0x66, //ILI9488 only supports 18-bit pixel format in 4/3 wire SPI mode + + + + // 5 frames + //ILI9XXX_ETMOD, 1, 0xC6, // + + + ILI9XXX_SLPOUT, 0x80, // Exit sleep mode + //ILI9XXX_INVON , 0, + ILI9XXX_DISPON, 0x80, // Set display on + 0x00 // end +}; + +static const uint8_t PROGMEM INITCMD_ST7796[] = { + // This ST7796S initilization routine was copied from https://github.com/prenticedavid/Adafruit_ST7796S_kbv/blob/master/Adafruit_ST7796S_kbv.cpp + ILI9XXX_SWRESET, 0x80, // Soft reset, then delay 150 ms + ILI9XXX_CSCON, 1, 0xC3, // ?? Unlock Manufacturer + ILI9XXX_CSCON, 1, 0x96, + ILI9XXX_VMCTR1, 1, 0x1C, //VCOM Control 1 [1C] + ILI9XXX_MADCTL, 1, 0x48, //Memory Access [00] + ILI9XXX_PIXFMT, 1, 0x55, //565 + ILI9XXX_IFMODE, 1, 0x80, //Interface [00] + ILI9XXX_INVCTR, 1, 0x01, //Inversion Control [01] + ILI9XXX_DFUNCTR, 3, 0x80, 0x02, 0x3B, // Display Function Control [80 02 3B] .kbv SS=1, NL=480 + ILI9XXX_ETMOD, 1, 0xC6, //Entry Mode [06] + + ILI9XXX_CSCON, 1, 0x69, //?? lock manufacturer commands + ILI9XXX_CSCON, 1, 0x3C, // + ILI9XXX_SLPOUT, 0x80, // Exit Sleep, then delay 150 ms + ILI9XXX_DISPON, 0x80, // Main screen turn on, delay 150 ms + 0x00 // End of list +}; + +// clang-format on +} // namespace ili9xxx +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b599eb2666..21cdb6bc6f 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2693,24 +2693,18 @@ display: row_start: 0 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ili9341 + - platform: ili9xxx model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 reset_pin: GPIO22 - led_pin: - number: GPIO15 - inverted: true lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - - platform: ili9341 + - platform: ili9xxx model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 reset_pin: GPIO22 - led_pin: - number: GPIO15 - inverted: true auto_clear_enabled: false rotation: 90 lambda: |- From a4f21db272e24c14d6f6a22a609042df0e91c740 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 9 Mar 2023 01:03:33 +0100 Subject: [PATCH 096/115] Drop broken logging macros (#4534) --- esphome/core/log.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/esphome/core/log.h b/esphome/core/log.h index b1b1cf9115..bef5e5c633 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -144,19 +144,12 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #endif #define ESP_LOGE(tag, ...) esph_log_e(tag, __VA_ARGS__) -#define LOG_E(tag, ...) ESP_LOGE(tag, __VA__ARGS__) #define ESP_LOGW(tag, ...) esph_log_w(tag, __VA_ARGS__) -#define LOG_W(tag, ...) ESP_LOGW(tag, __VA__ARGS__) #define ESP_LOGI(tag, ...) esph_log_i(tag, __VA_ARGS__) -#define LOG_I(tag, ...) ESP_LOGI(tag, __VA__ARGS__) #define ESP_LOGD(tag, ...) esph_log_d(tag, __VA_ARGS__) -#define LOG_D(tag, ...) ESP_LOGD(tag, __VA__ARGS__) #define ESP_LOGCONFIG(tag, ...) esph_log_config(tag, __VA_ARGS__) -#define LOG_CONFIG(tag, ...) ESP_LOGCONFIG(tag, __VA__ARGS__) #define ESP_LOGV(tag, ...) esph_log_v(tag, __VA_ARGS__) -#define LOG_V(tag, ...) ESP_LOGV(tag, __VA__ARGS__) #define ESP_LOGVV(tag, ...) esph_log_vv(tag, __VA_ARGS__) -#define LOG_VV(tag, ...) ESP_LOGVV(tag, __VA__ARGS__) #define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" #define BYTE_TO_BINARY(byte) \ From 7c91b4474ab3d31c7fcff125a4250fa7b81d30ce Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 9 Mar 2023 01:14:34 +0100 Subject: [PATCH 097/115] Revert storing Font glyphs in manually-allocated memory (#4516) This partially reverts commit 62459a8ae1d9a2182772d55ddf7d6ad983277f2e. --- esphome/components/display/display_buffer.cpp | 19 ++++++------------- esphome/components/display/display_buffer.h | 6 ++---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 85ebd2567b..420801f863 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -256,7 +256,7 @@ void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align if (glyph_n < 0) { // Unknown char, skip ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (font->get_glyphs_size() > 0) { + if (!font->get_glyphs().empty()) { uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width; for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) { for (int glyph_y = 0; glyph_y < height; glyph_y++) @@ -557,7 +557,7 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { } int Font::match_next_glyph(const char *str, int *match_length) { int lo = 0; - int hi = this->glyphs_size_ - 1; + int hi = this->glyphs_.size() - 1; while (lo != hi) { int mid = (lo + hi + 1) / 2; if (this->glyphs_[mid].compare_to(str)) { @@ -583,7 +583,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in int glyph_n = this->match_next_glyph(str + i, &match_length); if (glyph_n < 0) { // Unknown char, skip - if (this->glyphs_size_ > 0) + if (!this->get_glyphs().empty()) x += this->get_glyphs()[0].glyph_data_->width; i++; continue; @@ -604,16 +604,9 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *width = x - min_x; } Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->glyphs_ = allocator.allocate(data_nr); - if (this->glyphs_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate buffer for Glyphs!"); - return; - } - for (int i = 0; i < data_nr; ++i) { - this->glyphs_[i] = Glyph(data + i); - } - this->glyphs_size_ = data_nr; + glyphs_.reserve(data_nr); + for (int i = 0; i < data_nr; ++i) + glyphs_.emplace_back(&data[i]); } bool Image::get_pixel(int x, int y) const { diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 815ba8d2e1..0402826594 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -526,12 +526,10 @@ class Font { inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } - Glyph *&get_glyphs() { return this->glyphs_; } - const u_int16_t &get_glyphs_size() const { return this->glyphs_size_; } + const std::vector> &get_glyphs() const { return glyphs_; } protected: - Glyph *glyphs_{nullptr}; - u_int16_t glyphs_size_; + std::vector> glyphs_; int baseline_; int height_; }; From d82c6df57e7e2126026f73599142efcf220ff83b Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Thu, 9 Mar 2023 01:34:06 +0100 Subject: [PATCH 098/115] Correct BME680 gas calculation and heater_off (#4498) * Fix missing data array * Fix incorrect bit offset * Correct variable types * Do same conversions as in original library * Correct clang-format * Move out float conversion for clarity * Added check for heater stability * Correct clang format * Allow reporting gas resistance when heater is disabled * Correct clang format * Better error reporting by @DAVe3283 * Correct signed operation, range switching error was positive all the time --- esphome/components/bme680/bme680.cpp | 64 ++++++++++++++++++++-------- esphome/components/bme680/bme680.h | 8 ++-- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 64770ac0d5..e5704a8f9f 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -117,18 +117,24 @@ void BME680Component::setup() { this->calibration_.gh2 = cal2[12] << 8 | cal2[13]; this->calibration_.gh3 = cal2[15]; - if (!this->read_byte(0x02, &this->calibration_.res_heat_range)) { + uint8_t temp_var = 0; + if (!this->read_byte(0x02, &temp_var)) { this->mark_failed(); return; } - if (!this->read_byte(0x00, &this->calibration_.res_heat_val)) { + this->calibration_.res_heat_range = ((temp_var & 0x30) / 16); + + if (!this->read_byte(0x00, &temp_var)) { this->mark_failed(); return; } - if (!this->read_byte(0x04, &this->calibration_.range_sw_err)) { + this->calibration_.res_heat_val = (int8_t) temp_var; + + if (!this->read_byte(0x04, &temp_var)) { this->mark_failed(); return; } + this->calibration_.range_sw_err = ((int8_t) temp_var & (int8_t) 0xf0) / 16; this->calibration_.ambient_temperature = 25; // prime ambient temperature @@ -181,7 +187,7 @@ void BME680Component::setup() { return; } gas0_control &= ~0b00001000; - gas0_control |= heat_off ? 0b100 : 0b000; + gas0_control |= heat_off << 3; if (!this->write_byte(BME680_REGISTER_CONTROL_GAS0, gas0_control)) { this->mark_failed(); return; @@ -249,12 +255,12 @@ uint8_t BME680Component::calc_heater_resistance_(uint16_t temperature) { if (temperature > 400) temperature = 400; - const uint8_t ambient_temperature = this->calibration_.ambient_temperature; + const int8_t ambient_temperature = this->calibration_.ambient_temperature; const int8_t gh1 = this->calibration_.gh1; const int16_t gh2 = this->calibration_.gh2; const int8_t gh3 = this->calibration_.gh3; const uint8_t res_heat_range = this->calibration_.res_heat_range; - const uint8_t res_heat_val = this->calibration_.res_heat_val; + const int8_t res_heat_val = this->calibration_.res_heat_val; uint8_t heatr_res; int32_t var1; @@ -293,35 +299,57 @@ uint8_t BME680Component::calc_heater_duration_(uint16_t duration) { void BME680Component::read_data_() { uint8_t data[15]; if (!this->read_bytes(BME680_REGISTER_FIELD0, data, 15)) { + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(NAN); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(NAN); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(NAN); + if (this->gas_resistance_sensor_ != nullptr) + this->gas_resistance_sensor_->publish_state(NAN); + ESP_LOGW(TAG, "Communication with BME680 failed!"); this->status_set_warning(); return; } + this->status_clear_warning(); uint32_t raw_temperature = (uint32_t(data[5]) << 12) | (uint32_t(data[6]) << 4) | (uint32_t(data[7]) >> 4); uint32_t raw_pressure = (uint32_t(data[2]) << 12) | (uint32_t(data[3]) << 4) | (uint32_t(data[4]) >> 4); uint32_t raw_humidity = (uint32_t(data[8]) << 8) | uint32_t(data[9]); - uint16_t raw_gas = (uint16_t(data[13]) << 2) | (uint16_t(14) >> 6); + uint16_t raw_gas = (uint16_t)((uint32_t) data[13] * 4 | (((uint32_t) data[14]) / 64)); uint8_t gas_range = data[14] & 0x0F; float temperature = this->calc_temperature_(raw_temperature); float pressure = this->calc_pressure_(raw_pressure); float humidity = this->calc_humidity_(raw_humidity); - float gas_resistance = NAN; - if (data[14] & 0x20) { - gas_resistance = this->calc_gas_resistance_(raw_gas, gas_range); - } + float gas_resistance = this->calc_gas_resistance_(raw_gas, gas_range); + + bool gas_valid = (data[14] >> 5) & 1; + bool heat_stable = (data[14] >> 4) & 1; + if (this->heater_temperature_ == 0 || this->heater_duration_ == 0) + heat_stable = true; // Allow reporting gas resistance when heater is disabled ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure, humidity, gas_resistance); + if (!gas_valid) + ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!"); + if (!heat_stable) + ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)"); + if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) this->pressure_sensor_->publish_state(pressure); if (this->humidity_sensor_ != nullptr) this->humidity_sensor_->publish_state(humidity); - if (this->gas_resistance_sensor_ != nullptr) - this->gas_resistance_sensor_->publish_state(gas_resistance); - this->status_clear_warning(); + if (this->gas_resistance_sensor_ != nullptr) { + if (gas_valid && heat_stable) { + this->gas_resistance_sensor_->publish_state(gas_resistance); + } else { + this->status_set_warning(); + this->gas_resistance_sensor_->publish_state(NAN); + } + } } float BME680Component::calc_temperature_(uint32_t raw_temperature) { @@ -428,20 +456,22 @@ float BME680Component::calc_humidity_(uint16_t raw_humidity) { return calc_hum; } -uint32_t BME680Component::calc_gas_resistance_(uint16_t raw_gas, uint8_t range) { +float BME680Component::calc_gas_resistance_(uint16_t raw_gas, uint8_t range) { float calc_gas_res; float var1 = 0; float var2 = 0; float var3 = 0; + float raw_gas_f = raw_gas; + float range_f = 1U << range; const float range_sw_err = this->calibration_.range_sw_err; var1 = 1340.0f + (5.0f * range_sw_err); var2 = var1 * (1.0f + BME680_GAS_LOOKUP_TABLE_1[range] / 100.0f); var3 = 1.0f + (BME680_GAS_LOOKUP_TABLE_2[range] / 100.0f); - calc_gas_res = 1.0f / (var3 * 0.000000125f * float(1 << range) * (((float(raw_gas) - 512.0f) / var2) + 1.0f)); + calc_gas_res = 1.0f / (var3 * 0.000000125f * range_f * (((raw_gas_f - 512.0f) / var2) + 1.0f)); - return static_cast(calc_gas_res); + return calc_gas_res; } uint32_t BME680Component::calc_meas_duration_() { uint32_t tph_dur; // Calculate in us diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index 6446449742..cfa7aaca20 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -59,11 +59,11 @@ struct BME680CalibrationData { int8_t gh3; uint8_t res_heat_range; - uint8_t res_heat_val; - uint8_t range_sw_err; + int8_t res_heat_val; + int8_t range_sw_err; float tfine; - uint8_t ambient_temperature; + int8_t ambient_temperature; }; class BME680Component : public PollingComponent, public i2c::I2CDevice { @@ -117,7 +117,7 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice { /// Calculate the relative humidity in % using the provided raw ADC value. float calc_humidity_(uint16_t raw_humidity); /// Calculate the gas resistance in Ω using the provided raw ADC value. - uint32_t calc_gas_resistance_(uint16_t raw_gas, uint8_t range); + float calc_gas_resistance_(uint16_t raw_gas, uint8_t range); /// Calculate how long the sensor will take until we can retrieve data. uint32_t calc_meas_duration_(); From 1a9aedf1527703b2d451a0bf3802dc81caf2d059 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:47:57 +1300 Subject: [PATCH 099/115] Bump version to 2023.3.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a78fb6948f..a66dfbc0fd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.3.0b1" +__version__ = "2023.3.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From cdeb6e750f39a057f20dd2bafe880171924af58a Mon Sep 17 00:00:00 2001 From: jakehdk <94864542+jakehdk@users.noreply.github.com> Date: Sun, 12 Mar 2023 21:14:00 +0100 Subject: [PATCH 100/115] Add support for new clones of mpu6050 responding with 0x70 address (#4546) Co-authored-by: jakehdk --- esphome/components/mpu6050/mpu6050.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index cc426e58a2..51e3ec2383 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -23,7 +23,8 @@ const float GRAVITY_EARTH = 9.80665f; void MPU6050Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MPU6050..."); uint8_t who_am_i; - if (!this->read_byte(MPU6050_REGISTER_WHO_AM_I, &who_am_i) || (who_am_i != 0x68 && who_am_i != 0x98)) { + if (!this->read_byte(MPU6050_REGISTER_WHO_AM_I, &who_am_i) || + (who_am_i != 0x68 && who_am_i != 0x70 && who_am_i != 0x98)) { this->mark_failed(); return; } From bf79a700b769667cc738029987428464aa572077 Mon Sep 17 00:00:00 2001 From: Martin Murray Date: Sun, 12 Mar 2023 16:16:48 -0400 Subject: [PATCH 101/115] Add carbon dioxide device class to scd30 sensor schema. (#4547) --- esphome/components/scd30/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index ffbf90338f..1ddf0f1e85 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_CO2, CONF_UPDATE_INTERVAL, CONF_VALUE, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -46,6 +47,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( From b6f628ee40906d0ab59c0d46e7f1709cd824e5cd Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Sun, 12 Mar 2023 21:26:27 +0100 Subject: [PATCH 102/115] Feat: add support for hex color in color component (#4493) * Feat: add support for hex color in color component * Chore: move hex color validator to color component * Chore: add test * Chore: fix formatting * Chore: make linter happy * Chore: make linter happy * Fix: parse correct offsets Co-authored-by: Oxan van Leeuwen * Chore: use cv.Invalid * Fix: remove # because it indicates a comment in yaml * Fix: only allow hex if no other color value is set * Fix: tests * Fix: mutual exclusion of raw and hex colors * Chore: format file * Update __init__.py --------- Co-authored-by: Oxan van Leeuwen Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/color/__init__.py | 59 +++++++++++++++++++++------- tests/test1.yaml | 2 + 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py index 47679fcc68..9a85eace75 100644 --- a/esphome/components/color/__init__.py +++ b/esphome/components/color/__init__.py @@ -10,23 +10,42 @@ CONF_RED_INT = "red_int" CONF_GREEN_INT = "green_int" CONF_BLUE_INT = "blue_int" CONF_WHITE_INT = "white_int" - -CONFIG_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(ColorStruct), - cv.Exclusive(CONF_RED, "red"): cv.percentage, - cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t, - cv.Exclusive(CONF_GREEN, "green"): cv.percentage, - cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t, - cv.Exclusive(CONF_BLUE, "blue"): cv.percentage, - cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t, - cv.Exclusive(CONF_WHITE, "white"): cv.percentage, - cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t, - } -).extend(cv.COMPONENT_SCHEMA) +CONF_HEX = "hex" -async def to_code(config): +def hex_color(value): + if len(value) != 6: + raise cv.Invalid("Color must have six digits") + try: + return (int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)) + except ValueError as exc: + raise cv.Invalid("Color must be hexadecimal") from exc + + +CONFIG_SCHEMA = cv.Any( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Exclusive(CONF_RED, "red"): cv.percentage, + cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t, + cv.Exclusive(CONF_GREEN, "green"): cv.percentage, + cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t, + cv.Exclusive(CONF_BLUE, "blue"): cv.percentage, + cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t, + cv.Exclusive(CONF_WHITE, "white"): cv.percentage, + cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Required(CONF_HEX): hex_color, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +def from_rgbw(config): r = 0 if CONF_RED in config: r = int(config[CONF_RED] * 255) @@ -51,6 +70,16 @@ async def to_code(config): elif CONF_WHITE_INT in config: w = config[CONF_WHITE_INT] + return (r, g, b, w) + + +async def to_code(config): + if CONF_HEX in config: + r, g, b = config[CONF_HEX] + w = 0 + else: + r, g, b, w = from_rgbw(config) + cg.new_variable( config[CONF_ID], cg.StructInitializer(ColorStruct, ("r", r), ("g", g), ("b", b), ("w", w)), diff --git a/tests/test1.yaml b/tests/test1.yaml index 21cdb6bc6f..25c481db04 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2528,6 +2528,8 @@ color: red: 0% green: 1% blue: 100% + - id: kbx_green + hex: "3DEC55" display: - platform: lcd_gpio From 6e8e9c2aa9f0753a8c9c6fb5c947b279da2c9799 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:43:31 +1300 Subject: [PATCH 103/115] Allow AUTO_LOAD to be a function (#4550) --- esphome/loader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/loader.py b/esphome/loader.py index b245fa1610..cd21e5a509 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -71,7 +71,10 @@ class ComponentManifest: @property def auto_load(self) -> list[str]: - return getattr(self.module, "AUTO_LOAD", []) + al = getattr(self.module, "AUTO_LOAD", []) + if callable(al): + return al() + return al @property def codeowners(self) -> list[str]: From 4b7c233f1a00bf5b719b4c97a3a371f0761b6f4f Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 13 Mar 2023 01:13:19 +0100 Subject: [PATCH 104/115] On the ILI9xxx display's enable the psram on esp32 and allow big screen (#4551) * enable the psram on esp32 and allow big screen * update CODEOWNERS * small update * update CODEOWNERS again. * Removed the M5STACK because it is a ESP32 device. * i removed the wrong model * update the error message. --- esphome/components/ili9xxx/display.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 437fc93b89..780c64ec70 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -16,7 +16,13 @@ from esphome.const import ( ) DEPENDENCIES = ["spi"] -AUTO_LOAD = ["psram"] + + +def AUTO_LOAD(): + if CORE.is_esp32: + return ["psram"] + return [] + CODEOWNERS = ["@nielsnl68"] @@ -60,6 +66,16 @@ def _validate(config): raise cv.Invalid( "Providing color palette images requires palette mode to be 'IMAGE_ADAPTIVE'" ) + if CORE.is_esp8266 and config.get(CONF_MODEL) not in [ + "M5STACK", + "TFT_2.4", + "TFT_2.4R", + "ILI9341", + "ILI9342", + ]: + raise cv.Invalid( + "Provided model can't run on ESP8266. Use an ESP32 with PSRAM onboard" + ) return config From c149a3033c89f42c4791b25a97180d8c740fad20 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Mar 2023 13:13:36 +1300 Subject: [PATCH 105/115] Map gpio pins for touch on esp32-s2/s3 (#4552) * Map gpio pins for touch on esp32-s2/s3 * fix value --- .../components/esp32_touch/binary_sensor.py | 73 +++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index 326f559830..2cdf1343c3 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE from esphome.components import binary_sensor from esphome.const import ( CONF_PIN, @@ -7,6 +8,13 @@ from esphome.const import ( CONF_ID, ) from esphome.components.esp32 import gpio +from esphome.components.esp32.const import ( + KEY_ESP32, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) from . import esp32_touch_ns, ESP32TouchComponent DEPENDENCIES = ["esp32_touch", "esp32"] @@ -15,24 +23,63 @@ 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, + 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_touch_pad(value): value = gpio.validate_gpio_pin(value) - if value not in TOUCH_PADS: + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in TOUCH_PADS: + raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") + + pads = TOUCH_PADS[variant] + if value not in pads: raise cv.Invalid(f"Pin {value} does not support touch pads.") - return value + return cv.enum(pads)(value) ESP32TouchBinarySensor = esp32_touch_ns.class_( @@ -53,7 +100,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], ) From 65d2b806cc8f845d71748d370865bae759aeae12 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Mar 2023 13:32:21 +1300 Subject: [PATCH 106/115] Bump version to 2023.3.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a66dfbc0fd..3fa46f788e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.3.0b2" +__version__ = "2023.3.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 8a705bf4b042eb8aa8a29358e45a563e0492e5c5 Mon Sep 17 00:00:00 2001 From: Eduardo Roldan Date: Mon, 13 Mar 2023 19:46:46 -0300 Subject: [PATCH 107/115] pipsolar component. Correct the sscanf format for QPIG command parsing to set pv_input_voltage as float (not int) (#4165) --- esphome/components/pipsolar/pipsolar.cpp | 2 +- esphome/components/pipsolar/pipsolar.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index fab4705be7..5f203645fe 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -448,7 +448,7 @@ void Pipsolar::loop() { ESP_LOGD(TAG, "Decode QPIGS"); sscanf( // NOLINT tmp, // NOLINT - "(%f %f %f %f %d %d %d %d %f %d %d %d %d %f %f %d %1d%1d%1d%1d%1d%1d%1d%1d %d %d %d %1d%1d%1d", // NOLINT + "(%f %f %f %f %d %d %d %d %f %d %d %d %f %f %f %d %1d%1d%1d%1d%1d%1d%1d%1d %d %d %d %1d%1d%1d", // NOLINT &value_grid_voltage_, &value_grid_frequency_, &value_ac_output_voltage_, // NOLINT &value_ac_output_frequency_, // NOLINT &value_ac_output_apparent_power_, &value_ac_output_active_power_, &value_output_load_percent_, // NOLINT diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index 4f6edb4810..65fd3c670d 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -65,7 +65,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int) PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int) PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int) - PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float) PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float) PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float) PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int) From 0c7a3d1fffef6c9292b965bb0b9f8dc6790522e5 Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Mon, 13 Mar 2023 18:52:19 -0600 Subject: [PATCH 108/115] Revert "Remove state class from uptime sensor (#4345)" (#4557) This reverts commit 36c2e770bfd5b43a5471590be0d10033049eb3ea. Addresses esphome/issues#4193. --- esphome/components/uptime/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 50e584f5d5..07d7d8f2cf 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, DEVICE_CLASS_DURATION, @@ -16,6 +17,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_SECOND, icon=ICON_TIMER, accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_DURATION, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")) From 5e11469f50744ad3d01648cc449882f5cb425187 Mon Sep 17 00:00:00 2001 From: Stroe Andrei Catalin Date: Tue, 14 Mar 2023 02:54:35 +0200 Subject: [PATCH 109/115] Added response for Tuya RSSI command (#4549) * Added wifi rssi util Added tuya mcu response to wifi rssi command * Cleanup * PR Comments * PR Comments --- esphome/components/tuya/tuya.cpp | 17 +++++++++++++++++ esphome/components/tuya/tuya.h | 2 ++ 2 files changed, 19 insertions(+) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index fad4bb0bac..79a9049b04 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -5,6 +5,10 @@ #include "esphome/core/util.h" #include "esphome/core/gpio.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + #ifdef USE_CAPTIVE_PORTAL #include "esphome/components/captive_portal/captive_portal.h" #endif @@ -234,6 +238,10 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff case TuyaCommandType::WIFI_TEST: this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector{0x00, 0x00}}); break; + case TuyaCommandType::WIFI_RSSI: + this->send_command_( + TuyaCommand{.cmd = TuyaCommandType::WIFI_RSSI, .payload = std::vector{get_wifi_rssi_()}}); + break; case TuyaCommandType::LOCAL_TIME_QUERY: #ifdef USE_TIME if (this->time_id_.has_value()) { @@ -475,6 +483,15 @@ uint8_t Tuya::get_wifi_status_code_() { return status; } +uint8_t Tuya::get_wifi_rssi_() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->wifi_rssi(); +#endif + + return 0; +} + void Tuya::send_wifi_status_() { uint8_t status = this->get_wifi_status_code_(); diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index b9c917f672..8d6153482f 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -55,6 +55,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, + WIFI_RSSI = 0x24, VACUUM_MAP_UPLOAD = 0x28, GET_NETWORK_STATUS = 0x2B, }; @@ -123,6 +124,7 @@ class Tuya : public Component, public uart::UARTDevice { void set_status_pin_(); void send_wifi_status_(); uint8_t get_wifi_status_code_(); + uint8_t get_wifi_rssi_(); #ifdef USE_TIME void send_local_time_(); From 9a7af97b2df059159a3eaa632206dbec031e74ef Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Mar 2023 14:11:55 +1300 Subject: [PATCH 110/115] Bump version to 2023.3.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3fa46f788e..08182728a2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.3.0b3" +__version__ = "2023.3.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 83f8e8424779f56c1244de4001ac3e9509dfc4fe Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 15 Mar 2023 00:21:23 -0500 Subject: [PATCH 111/115] Remove switch actions during config; bump setup priority (#4563) --- esphome/components/sprinkler/sprinkler.cpp | 6 ------ esphome/components/sprinkler/sprinkler.h | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 9d3044802d..d73d8d8fbf 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -477,7 +477,6 @@ void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *val if (this->is_a_valid_valve(valve_number)) { this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); this->valve_[valve_number].run_duration = run_duration; - valve_switch->turn_off(); } } @@ -489,8 +488,6 @@ void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Swit this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); this->valve_[valve_number].run_duration = run_duration; - valve_switch_off->turn_off(); - valve_switch_on->turn_off(); } } @@ -505,7 +502,6 @@ void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch this->pump_.resize(this->pump_.size() + 1); this->pump_.back().set_on_switch(pump_switch); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump - pump_switch->turn_off(); } } @@ -524,8 +520,6 @@ void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_: this->pump_.back().set_on_switch(pump_switch_on); this->pump_.back().set_pulse_duration(pulse_duration); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump - pump_switch_off->turn_off(); - pump_switch_on->turn_off(); } } diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 1b8c7e4528..1cde60321d 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -103,7 +103,7 @@ class SprinklerControllerNumber : public number::Number, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } Trigger *get_set_trigger() const { return set_trigger_; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } From 11567085d8e78888b8a73a683c15a890233fd0bc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Mar 2023 20:42:33 +1300 Subject: [PATCH 112/115] Mark esp32_touch supported only on standard esp32 variant (#4562) * Mark esp32_touch supported only on standard esp32 variant * Add back default --- esphome/components/esp32_touch/__init__.py | 56 ++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index cdf6aa3abd..3c9bef9665 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_VOLTAGE_ATTENUATION, ) from esphome.core import TimePeriod +from esphome.components import esp32 AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] @@ -50,30 +51,37 @@ VOLTAGE_ATTENUATION = { "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, } -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) +CONFIG_SCHEMA = cv.All( + 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), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32, + ] + ), +) async def to_code(config): From fce99d4b17738fe9bb53e272b6718621255c44a7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Mar 2023 21:20:08 +1300 Subject: [PATCH 113/115] Bump version to 2023.3.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 08182728a2..6887632ed5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.3.0b4" +__version__ = "2023.3.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 5ffdc668642fc2812f36e275a8812af844a2580d Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Wed, 15 Mar 2023 20:45:50 +0100 Subject: [PATCH 114/115] fixing `shrink` and `extend` functions of the displaybuffer's Rect class (#4565) * fixing rectangle's `shrink` and `extend` * fixed the rect::shrink and rect::inside methods and added rect:equal() method * fixed internal clang issue again. When would is this going to be fixed :( * fixed internal clang issue again. When would is this going to be fixed :( * remove trailing space --- esphome/components/display/display_buffer.cpp | 33 ++++++++++++------- esphome/components/display/display_buffer.h | 5 +-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 420801f863..7cd85dabd4 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -32,9 +32,11 @@ void Rect::extend(Rect rect) { this->h = rect.h; } else { if (this->x > rect.x) { + this->w = this->w + (this->x - rect.x); this->x = rect.x; } if (this->y > rect.y) { + this->h = this->h + (this->y - rect.y); this->y = rect.y; } if (this->x2() < rect.x2()) { @@ -49,29 +51,35 @@ void Rect::shrink(Rect rect) { if (!this->inside(rect)) { (*this) = Rect(); } else { - if (this->x < rect.x) { - this->x = rect.x; - } - if (this->y < rect.y) { - this->y = rect.y; - } if (this->x2() > rect.x2()) { this->w = rect.x2() - this->x; } + if (this->x < rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } if (this->y2() > rect.y2()) { this->h = rect.y2() - this->y; } + if (this->y < rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } } } -bool Rect::inside(int16_t x, int16_t y, bool absolute) { // NOLINT +bool Rect::equal(Rect rect) { + return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); +} + +bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT if (!this->is_set()) { return true; } if (absolute) { - return ((x >= 0) && (x <= this->w) && (y >= 0) && (y <= this->h)); + return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); } else { - return ((x >= this->x) && (x <= this->x2()) && (y >= this->y) && (y <= this->y2())); + return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); } } @@ -80,15 +88,16 @@ bool Rect::inside(Rect rect, bool absolute) { return true; } if (absolute) { - return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); - } else { return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } else { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); } } void Rect::info(const std::string &prefix) { if (this->is_set()) { - ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d]", prefix.c_str(), this->x, this->y, this->w, this->h); + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), + this->y2()); } else ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 0402826594..4477685e1b 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -120,8 +120,9 @@ class Rect { void extend(Rect rect); void shrink(Rect rect); - bool inside(Rect rect, bool absolute = false); - bool inside(int16_t x, int16_t y, bool absolute = false); + bool inside(Rect rect, bool absolute = true); + bool inside(int16_t test_x, int16_t test_y, bool absolute = true); + bool equal(Rect rect); void info(const std::string &prefix = "rect info:"); }; From c3d9eef01f424951d5a1786984478ac3e8aa9d7d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Mar 2023 08:57:54 +1300 Subject: [PATCH 115/115] Bump version to 2023.3.0b6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6887632ed5..ee7b8e96b1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.3.0b5" +__version__ = "2023.3.0b6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"