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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c32dc11b61..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 @@ -181,9 +182,22 @@ 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 # 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5ae80da3b..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: @@ -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/CODEOWNERS b/CODEOWNERS index ca1da2f153..3d6ea5cd32 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 @@ -24,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 @@ -90,11 +92,13 @@ 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 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 @@ -113,11 +117,13 @@ 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 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 @@ -160,8 +166,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 @@ -208,6 +215,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/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 diff --git a/docker/Dockerfile b/docker/Dockerfile index ddc666cf6a..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/* \ @@ -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/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/codegen.py b/esphome/codegen.py index ef5b490004..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, @@ -63,6 +64,7 @@ from esphome.cpp_types import ( # noqa uint16, uint32, uint64, + int16, int32, int64, size_t, 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/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/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/api/api.proto b/esphome/components/api/api.proto index ffb3bcb07e..1cebdd0cbe 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; @@ -1338,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 65659941d6..40a5a230a5 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(); @@ -951,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 9df05d2978..381f8b3c46 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 @@ -5964,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 2db1c6fafa..e9025142e9 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; @@ -1527,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/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/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/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/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 600c9efe5a..f4a5c95b12 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, @@ -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/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. 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, ), } ) 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/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/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}; diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index b0611a62e9..55f2fe794a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -11,16 +11,19 @@ from esphome.const import ( CONF_ON_PRESS, CONF_TRIGGER_ID, CONF_MQTT_ID, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) 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 DEVICE_CLASSES = [ + DEVICE_CLASS_EMPTY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ] @@ -54,30 +57,23 @@ _UNDEF = object() def button_schema( + class_: MockObjClass, + *, icon: str = _UNDEF, entity_category: str = _UNDEF, device_class: str = _UNDEF, ) -> cv.Schema: - schema = BUTTON_SCHEMA - 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/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/climate/__init__.py b/esphome/components/climate/__init__.py index eaa87afcb1..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, @@ -104,9 +105,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()) +ControlTrigger = climate_ns.class_("ControlTrigger", 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( { @@ -116,9 +148,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( @@ -175,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), @@ -193,7 +228,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/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 e1611d2fa9..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; @@ -430,9 +435,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 +449,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 +549,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..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. @@ -241,7 +249,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; @@ -285,10 +293,12 @@ 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_{}; - 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/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/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/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/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/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", } 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, + ], } 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/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_; }; diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f30fa9a7b2..62021afea9 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 @@ -138,18 +168,18 @@ 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, 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 -ESP_IDF_PLATFORM_VERSION = cv.Version(5, 2, 0) +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), } @@ -183,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), } @@ -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,32 @@ def copy_files(): __version__, ) + import shutil + + shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) + + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: + 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/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/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_; 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}; }; 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/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_; 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/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/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/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/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/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/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/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/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/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/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/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/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/__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/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/__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/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; 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)) 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/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 8dd03dd5c8..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"; @@ -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)) { @@ -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/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/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/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/number/__init__.py b/esphome/components/number/__init__.py index 463557e3b3..70c53cace3 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, @@ -17,19 +19,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 +52,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,20 +60,24 @@ 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 +from esphome.cpp_generator import MockObjClass 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 +85,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 +95,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, @@ -140,13 +153,12 @@ 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( { 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), @@ -160,12 +172,36 @@ 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, + *, + icon: str = _UNDEF, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, + unit_of_measurement: str = _UNDEF, +) -> cv.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_( 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/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/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/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."); 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/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 6a31a723a1..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) { @@ -37,6 +38,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 +48,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 +59,14 @@ 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_); +} + +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 diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index dff904476b..21676e3422 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -14,8 +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_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } - void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void update() override; @@ -23,12 +22,23 @@ 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 diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 08ec688afb..097b1c1cfd 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -1,18 +1,24 @@ 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, 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"] @@ -20,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( { @@ -42,6 +51,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")) @@ -49,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) @@ -66,3 +95,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/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index c3149ce430..4d9196c9c5 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): @@ -710,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_)) 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])) 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/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/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/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 7842cef4de..d3cb39c2f6 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, @@ -262,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_) 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/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): 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_); } 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 {}; } 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 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", 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/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/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(); 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), } 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; } 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; 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, ) 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): diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 89a687e5a6..fad4bb0bac 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,19 @@ 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_(); + + 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 +454,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 +464,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..b9c917f672 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -55,6 +55,8 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, LOCAL_TIME_QUERY = 0x1C, + VACUUM_MAP_UPLOAD = 0x28, + GET_NETWORK_STATUS = 0x2B, }; enum class TuyaInitState : uint8_t { @@ -120,6 +122,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_(); 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), } ), ), 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"]; } 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; 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 ce3eac9ed6..a78fb6948f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.2.0b5" +__version__ = "2023.3.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -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" @@ -447,6 +448,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" @@ -797,6 +799,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" @@ -856,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" @@ -877,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" @@ -916,80 +923,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 = "" @@ -1009,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/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: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index b36a64b82a..daa09b912e 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_; }; @@ -319,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(); 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/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 477dadc187..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. @@ -612,7 +653,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) 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)) 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}; 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/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()) 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") diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index f7d471586d..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)) @@ -985,7 +985,6 @@ class LogoutHandler(BaseHandler): class SecretKeysRequestHandler(BaseHandler): @authenticated def get(self): - filename = None for secret_filename in const.SECRETS_FILES: 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) 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/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 diff --git a/platformio.ini b/platformio.ini index b92a1407ed..65dccde3b9 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.40404.0 framework = espidf lib_deps = @@ -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} diff --git a/requirements.txt b/requirements.txt index 9f4d34528c..df041db26f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,12 @@ 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 -esptool==4.4 +platformio==6.1.6 # When updating platformio, also update Dockerfile +esptool==4.5.1 click==8.1.3 esphome-dashboard==20230214.0 -aioesphomeapi==13.1.0 -zeroconf==0.47.1 +aioesphomeapi==13.5.0 +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 diff --git a/requirements_test.txt b/requirements_test.txt index 8404818c95..771086421e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,11 +1,11 @@ -pylint==2.15.10 +pylint==2.16.4 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 +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 # 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 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() 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/test1.yaml b/tests/test1.yaml index a77f8802b9..b599eb2666 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 @@ -437,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: @@ -1219,6 +1247,18 @@ sensor: name: "Still Energy" detection_distance: name: "Distance Detection" + - platform: sen21231 + name: "Person Sensor" + i2c_id: i2c_bus + - platform: fs3000 + name: "Air Velocity" + 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 @@ -2048,6 +2088,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 diff --git a/tests/test3.yaml b/tests/test3.yaml index 4827b7cbcd..f1b64b3389 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.csv on_boot: - if: condition: @@ -283,6 +285,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 @@ -573,12 +579,15 @@ sensor: power_factor: name: PZEMAC Power Factor - platform: pzemdc + id: pzemdc1 voltage: name: PZEMDC Voltage current: name: PZEMDC Current power: name: PZEMDC Power + energy: + name: PZEMDC Energy - platform: tmp102 name: TMP102 Temperature - platform: hm3301 @@ -807,6 +816,12 @@ sensor: temperature_1: name: Temperature 1 + - platform: kuntze + ph: + name: Kuntze pH + temperature: + name: Kuntze temperature + time: - platform: homeassistant @@ -911,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 @@ -1188,8 +1208,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 diff --git a/tests/test5.yaml b/tests/test5.yaml index 5f72579d08..ec3fef5e19 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 @@ -475,7 +477,6 @@ sensor: acceleration_mode: low store_baseline: true address: 0x69 - - platform: mcp9600 thermocouple_type: K hot_junction: @@ -518,6 +519,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: 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" 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