diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3baaf02506..c32dc11b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,10 @@ jobs: file: tests/test6.yaml name: Test tests/test6.yaml pio_cache_key: test6 + - id: test + file: tests/test7.yaml + name: Test tests/test7.yaml + pio_cache_key: test7 - id: pytest name: Run pytest - id: clang-format diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3b96141c4..3904834dc9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,8 +67,10 @@ jobs: contents: read packages: write runs-on: ubuntu-latest + continue-on-error: ${{ matrix.image.title == 'lint' }} needs: [init] strategy: + fail-fast: false matrix: image: - title: "ha-addon" @@ -136,14 +138,18 @@ jobs: runs-on: ubuntu-latest needs: [deploy-docker] steps: - - env: - TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} - # yamllint disable rule:line-length - run: | - curl \ - -u ":$TOKEN" \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/esphome/home-assistant-addon/actions/workflows/bump-version.yml/dispatches \ - -d '{"ref":"main","inputs":{"version":"${{ github.event.release.tag_name }}","content":${{ toJSON(github.event.release.body) }}}}' - # yamllint enable rule:line-length + - name: Trigger Workflow + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} + script: | + github.rest.actions.createWorkflowDispatch({ + owner: "esphome", + repo: "home-assistant-addon", + workflow_id: "bump-version.yml", + ref: "main", + inputs: { + version: "${{ github.event.release.tag_name }}", + content: ${{ toJSON(github.event.release.body) }} + } + }) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6cb3b659c4..a2ba086394 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +38,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v6 + - uses: actions/stale@v7 with: days-before-pr-stale: -1 days-before-pr-close: -1 diff --git a/.gitignore b/.gitignore index 110437c368..71b66b2499 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ tests/.esphome/ sdkconfig.* !sdkconfig.defaults + +.tests/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8c0a22e81..e5ae80da3b 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.10.0 + rev: 22.12.0 hooks: - id: black args: diff --git a/CODEOWNERS b/CODEOWNERS index d30060fe3b..ca1da2f153 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,6 +41,8 @@ esphome/components/ble_client/* @buxtronix esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmp3xx/* @martgras +esphome/components/bp1658cj/* @Cossid +esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @MrEditor97 @@ -69,6 +71,7 @@ esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core @@ -100,9 +103,11 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey +esphome/components/hte501/* @Stock-M esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core esphome/components/i2s_audio/* @jesserockz +esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -111,13 +116,17 @@ esphome/components/integration/* @OttoWinter 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/lcd_menu/* @numo68 +esphome/components/ld2410/* @sebcaps esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny +esphome/components/matrix_keypad/* @ssieb esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger esphome/components/max7219digit/* @rspaargaren @@ -138,6 +147,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey @@ -164,6 +174,8 @@ esphome/components/nfc/* @jesserockz esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core +esphome/components/pca9554/* @hwstar +esphome/components/pcf85063/* @brogon esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie @@ -204,8 +216,12 @@ esphome/components/sgp4x/* @SenexCrenshaw @martgras esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet +esphome/components/sigma_delta_output/* @Cat-Ion esphome/components/sim800l/* @glmnet +esphome/components/sm10bit_base/* @Cossid esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/sm2235/* @Cossid +esphome/components/sm2335/* @Cossid esphome/components/sml/* @alengwenus esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz @@ -234,6 +250,7 @@ esphome/components/switch/* @esphome/core esphome/components/t6615/* @tylermenezes esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet +esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter @@ -258,12 +275,15 @@ esphome/components/uart/* @esphome/core esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter +esphome/components/vbus/* @ssieb esphome/components/version/* @esphome/core esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze +esphome/components/wiegand/* @ssieb esphome/components/wl_134/* @hobbypunk90 +esphome/components/x9c/* @EtienneMD esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/docker/Dockerfile b/docker/Dockerfile index 66b708f522..ddc666cf6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ RUN \ python3-cryptography=3.3.2-1 \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ - curl=7.74.0-1.3+deb11u3 \ + curl=7.74.0-1.3+deb11u5 \ openssh-client=1:8.4p1-5+deb11u1 \ && rm -rf \ /tmp/* \ diff --git a/docker/build.py b/docker/build.py index ae977f87c1..47461ddf97 100755 --- a/docker/build.py +++ b/docker/build.py @@ -8,32 +8,49 @@ import re import sys -CHANNEL_DEV = 'dev' -CHANNEL_BETA = 'beta' -CHANNEL_RELEASE = 'release' +CHANNEL_DEV = "dev" +CHANNEL_BETA = "beta" +CHANNEL_RELEASE = "release" CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] -ARCH_AMD64 = 'amd64' -ARCH_ARMV7 = 'armv7' -ARCH_AARCH64 = 'aarch64' +ARCH_AMD64 = "amd64" +ARCH_ARMV7 = "armv7" +ARCH_AARCH64 = "aarch64" ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] -TYPE_DOCKER = 'docker' -TYPE_HA_ADDON = 'ha-addon' -TYPE_LINT = 'lint' +TYPE_DOCKER = "docker" +TYPE_HA_ADDON = "ha-addon" +TYPE_LINT = "lint" TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] parser = argparse.ArgumentParser() -parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag") -parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") -parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") -parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") -subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) +parser.add_argument( + "--tag", + type=str, + required=True, + help="The main docker tag to push to. If a version number also adds latest and/or beta tag", +) +parser.add_argument( + "--arch", choices=ARCHS, required=False, help="The architecture to build for" +) +parser.add_argument( + "--build-type", choices=TYPES, required=True, help="The type of build to run" +) +parser.add_argument( + "--dry-run", action="store_true", help="Don't run any commands, just print them" +) +subparsers = parser.add_subparsers( + help="Action to perform", dest="command", required=True +) build_parser = subparsers.add_parser("build", help="Build the image") build_parser.add_argument("--push", help="Also push the images", action="store_true") -build_parser.add_argument("--load", help="Load the docker image locally", action="store_true") -manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") +build_parser.add_argument( + "--load", help="Load the docker image locally", action="store_true" +) +manifest_parser = subparsers.add_parser( + "manifest", help="Create a manifest from already pushed images" +) @dataclass(frozen=True) @@ -49,7 +66,7 @@ class DockerParams: prefix = { TYPE_DOCKER: "esphome/esphome", TYPE_HA_ADDON: "esphome/esphome-hassio", - TYPE_LINT: "esphome/esphome-lint" + TYPE_LINT: "esphome/esphome-lint", }[build_type] build_to = f"{prefix}-{arch}" baseimgtype = { @@ -128,13 +145,21 @@ def main(): # 3. build cmd = [ - "docker", "buildx", "build", - "--build-arg", f"BASEIMGTYPE={params.baseimgtype}", - "--build-arg", f"BUILD_VERSION={args.tag}", - "--cache-from", f"type=registry,ref={cache_img}", - "--file", "docker/Dockerfile", - "--platform", params.platform, - "--target", params.target, + "docker", + "buildx", + "build", + "--build-arg", + f"BASEIMGTYPE={params.baseimgtype}", + "--build-arg", + f"BUILD_VERSION={args.tag}", + "--cache-from", + f"type=registry,ref={cache_img}", + "--file", + "docker/Dockerfile", + "--platform", + params.platform, + "--target", + params.target, ] for img in imgs: cmd += ["--tag", img] @@ -160,9 +185,7 @@ def main(): run_command(*cmd) # 2. Push manifests for target in targets: - run_command( - "docker", "manifest", "push", target - ) + run_command("docker", "manifest", "push", target) if __name__ == "__main__": diff --git a/esphome/__main__.py b/esphome/__main__.py index 9b6043ef50..24c2ce1d13 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -339,7 +339,7 @@ def command_config(args, config): _LOGGER.info("Configuration is valid!") if not CORE.verbose: config = strip_default_ids(config) - safe_print(yaml_util.dump(config)) + safe_print(yaml_util.dump(config, args.show_secrets)) return 0 @@ -665,6 +665,9 @@ def parse_args(argv): parser_config.add_argument( "configuration", help="Your YAML configuration file(s).", nargs="+" ) + parser_config.add_argument( + "--show-secrets", help="Show secrets in output.", action="store_true" + ) parser_compile = subparsers.add_parser( "compile", help="Read the configuration and compile a program." diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index beb379db93..c3f3c00c63 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "ads1115"; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; -static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; +static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; // 3300_SPS for ADS1015 void ADS1115Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); @@ -18,6 +18,9 @@ void ADS1115Component::setup() { this->mark_failed(); return; } + + ESP_LOGCONFIG(TAG, "Configuring ADS1115..."); + uint16_t config = 0; // Clear single-shot bit // 0b0xxxxxxxxxxxxxxx @@ -77,6 +80,7 @@ void ADS1115Component::dump_config() { LOG_SENSOR(" ", "Sensor", sensor); ESP_LOGCONFIG(TAG, " Multiplexer: %u", sensor->get_multiplexer()); ESP_LOGCONFIG(TAG, " Gain: %u", sensor->get_gain()); + ESP_LOGCONFIG(TAG, " Resolution: %u", sensor->get_resolution()); } } float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { @@ -127,27 +131,45 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { this->status_set_warning(); return NAN; } + + if (sensor->get_resolution() == ADS1015_12_BITS) { + bool negative = (raw_conversion >> 15) == 1; + + // shift raw_conversion as it's only 12-bits, left justified + raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); + + // check if number was negative in order to keep the sign + if (negative) { + // the number was negative + // 1) set the negative bit back + raw_conversion |= 0x8000; + // 2) reset the former (shifted) negative bit + raw_conversion &= 0xF7FF; + } + } + auto signed_conversion = static_cast(raw_conversion); float millivolts; + float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f; switch (sensor->get_gain()) { case ADS1115_GAIN_6P144: - millivolts = signed_conversion * 0.187500f; + millivolts = (signed_conversion * 6144) / divider; break; case ADS1115_GAIN_4P096: - millivolts = signed_conversion * 0.125000f; + millivolts = (signed_conversion * 4096) / divider; break; case ADS1115_GAIN_2P048: - millivolts = signed_conversion * 0.062500f; + millivolts = (signed_conversion * 2048) / divider; break; case ADS1115_GAIN_1P024: - millivolts = signed_conversion * 0.031250f; + millivolts = (signed_conversion * 1024) / divider; break; case ADS1115_GAIN_0P512: - millivolts = signed_conversion * 0.015625f; + millivolts = (signed_conversion * 512) / divider; break; case ADS1115_GAIN_0P256: - millivolts = signed_conversion * 0.007813f; + millivolts = (signed_conversion * 256) / divider; break; default: millivolts = NAN; diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index 17d5a910d8..0b8bfb339b 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -30,6 +30,11 @@ enum ADS1115Gain { ADS1115_GAIN_0P256 = 0b101, }; +enum ADS1115Resolution { + ADS1115_16_BITS = 16, + ADS1015_12_BITS = 12, +}; + class ADS1115Sensor; class ADS1115Component : public Component, public i2c::I2CDevice { @@ -58,15 +63,17 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol void update() override; void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } void set_gain(ADS1115Gain gain) { gain_ = gain; } - + void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; } float sample() override; uint8_t get_multiplexer() const { return multiplexer_; } uint8_t get_gain() const { return gain_; } + uint8_t get_resolution() const { return resolution_; } protected: ADS1115Component *parent_; ADS1115Multiplexer multiplexer_; ADS1115Gain gain_; + ADS1115Resolution resolution_; }; } // namespace ads1115 diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index 190e641ca3..f0d894e2af 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler from esphome.const import ( CONF_GAIN, CONF_MULTIPLEXER, + CONF_RESOLUTION, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -35,6 +36,12 @@ GAIN = { "0.256": ADS1115Gain.ADS1115_GAIN_0P256, } +ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution") +RESOLUTION = { + "16_BITS": ADS1115Resolution.ADS1115_16_BITS, + "12_BITS": ADS1115Resolution.ADS1015_12_BITS, +} + def validate_gain(value): if isinstance(value, float): @@ -63,6 +70,9 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), cv.Required(CONF_GAIN): validate_gain, + cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( + RESOLUTION, upper=True, space="_" + ), } ) .extend(cv.polling_component_schema("60s")) @@ -77,5 +87,6 @@ async def to_code(config): cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_resolution(config[CONF_RESOLUTION])) cg.add(paren.register_sensor(var)) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 87d72254e8..ce9f057496 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -117,7 +117,7 @@ async def to_code(config): data[pos] = rgb & 255 pos += 1 - elif config[CONF_TYPE] == "BINARY": + elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: width8 = ((width + 7) // 8) * 8 data = [0 for _ in range((height * width8 // 8) * frames)] for frameIndex in range(frames): diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 05091f3f7d..1c6ec5c14a 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -23,7 +23,7 @@ void APDS9960::setup() { return; } - if (id != 0xAB && id != 0x9C) { // APDS9960 all should have one of these IDs + if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs this->error_code_ = WRONG_ID; this->mark_failed(); return; diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e1bc7b0a57..ffb3bcb07e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -206,6 +206,8 @@ message DeviceInfoResponse { uint32 bluetooth_proxy_version = 11; string manufacturer = 12; + + string friendly_name = 13; } message ListEntitiesRequest { @@ -785,6 +787,7 @@ enum ClimateFanMode { CLIMATE_FAN_MIDDLE = 6; CLIMATE_FAN_FOCUS = 7; CLIMATE_FAN_DIFFUSE = 8; + CLIMATE_FAN_QUIET = 9; } enum ClimateSwingMode { CLIMATE_SWING_OFF = 0; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index aac58587d1..65659941d6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -930,6 +930,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; resp.uses_password = this->parent_->uses_password(); resp.name = App.get_name(); + resp.friendly_name = App.get_friendly_name(); resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index b19a55764f..c18e045a99 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -616,6 +616,9 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { struct iovec iov[2]; iov[0].iov_base = header; iov[0].iov_len = 3; + if (len == 0) { + return write_raw_(iov, 1); + } iov[1].iov_base = const_cast(data); iov[1].iov_len = len; @@ -913,6 +916,9 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay struct iovec iov[2]; iov[0].iov_base = &header[0]; iov[0].iov_len = header.size(); + if (payload_len == 0) { + return write_raw_(iov, 1); + } iov[1].iov_base = const_cast(payload); iov[1].iov_len = payload_len; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f108d38e8f..9df05d2978 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -235,6 +235,8 @@ template<> const char *proto_enum_to_string(enums::Climat return "CLIMATE_FAN_FOCUS"; case enums::CLIMATE_FAN_DIFFUSE: return "CLIMATE_FAN_DIFFUSE"; + case enums::CLIMATE_FAN_QUIET: + return "CLIMATE_FAN_QUIET"; default: return "UNKNOWN"; } @@ -628,6 +630,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->manufacturer = value.as_string(); return true; } + case 13: { + this->friendly_name = value.as_string(); + return true; + } default: return false; } @@ -645,6 +651,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(10, this->webserver_port); buffer.encode_uint32(11, this->bluetooth_proxy_version); buffer.encode_string(12, this->manufacturer); + buffer.encode_string(13, this->friendly_name); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -699,6 +706,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" manufacturer: "); out.append("'").append(this->manufacturer).append("'"); out.append("\n"); + + out.append(" friendly_name: "); + out.append("'").append(this->friendly_name).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8a78f1ad03..2db1c6fafa 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -99,6 +99,7 @@ enum ClimateFanMode : uint32_t { CLIMATE_FAN_MIDDLE = 6, CLIMATE_FAN_FOCUS = 7, CLIMATE_FAN_DIFFUSE = 8, + CLIMATE_FAN_QUIET = 9, }; enum ClimateSwingMode : uint32_t { CLIMATE_SWING_OFF = 0, @@ -276,6 +277,7 @@ class DeviceInfoResponse : public ProtoMessage { uint32_t webserver_port{0}; uint32_t bluetooth_proxy_version{0}; std::string manufacturer{}; + std::string friendly_name{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp index 02ac289e0e..e272241040 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.cpp +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -37,9 +37,13 @@ void BedJetFan::control(const fan::FanCall &call) { // ignore speed changes if not on or turning on if (this->state && call.get_speed().has_value()) { - this->speed = *call.get_speed(); - this->parent_->set_fan_index(this->speed); - did_change = true; + auto speed = *call.get_speed(); + if (speed >= 1) { + this->speed = speed; + // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1 + this->parent_->set_fan_index(this->speed - 1); + did_change = true; + } } if (did_change) { @@ -57,8 +61,9 @@ void BedJetFan::on_status(const BedjetStatusPacket *data) { did_change = true; } - if (data->fan_step != this->speed) { - this->speed = data->fan_step; + // BedjetStatusPacket.fan_step is in range 0-19, but Fan.speed wants 1-20. + if (data->fan_step + 1 != this->speed) { + this->speed = data->fan_step + 1; did_change = true; } diff --git a/esphome/components/bp1658cj/__init__.py b/esphome/components/bp1658cj/__init__.py new file mode 100644 index 0000000000..8388b16df9 --- /dev/null +++ b/esphome/components/bp1658cj/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels" +CONF_MAX_POWER_WHITE_CHANNELS = "max_power_white_channels" + +AUTO_LOAD = ["output"] +bp1658cj_ns = cg.esphome_ns.namespace("bp1658cj") +BP1658CJ = bp1658cj_ns.class_("BP1658CJ", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BP1658CJ), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_MAX_POWER_COLOR_CHANNELS, default=2): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_MAX_POWER_WHITE_CHANNELS, default=4): cv.int_range( + min=0, max=15 + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS])) + cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS])) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp new file mode 100644 index 0000000000..5b9e4a5a2c --- /dev/null +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -0,0 +1,110 @@ +#include "bp1658cj.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bp1658cj { + +static const char *const TAG = "bp1658cj"; + +static const uint8_t BP1658CJ_MODEL_ID = 0x80; +static const uint8_t BP1658CJ_ADDR_STANDBY = 0x0; +static const uint8_t BP1658CJ_ADDR_START_3CH = 0x10; +static const uint8_t BP1658CJ_ADDR_START_2CH = 0x20; +static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30; + +void BP1658CJ::setup() { + ESP_LOGCONFIG(TAG, "Setting up BP1658CJ Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(false); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(false); + this->pwm_amounts_.resize(5, 0); +} +void BP1658CJ::dump_config() { + ESP_LOGCONFIG(TAG, "BP1658CJ:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Color Channels Max Power: %u", this->max_power_color_channels_); + ESP_LOGCONFIG(TAG, " White Channels Max Power: %u", this->max_power_white_channels_); +} + +void BP1658CJ::loop() { + if (!this->update_) + return; + + uint8_t data[12]; + if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Off / Sleep + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY; + for (int i = 1; i < 12; i++) + data[i] = 0; + this->write_buffer_(data, 12); + } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { + // Only data on white channels + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_2CH; + data[1] = 0 << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 12); + } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Only data on RGB channels + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_3CH; + data[1] = this->max_power_color_channels_ << 4 | 0; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 12); + } else { + // All channels + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_5CH; + data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 12); + } + + this->update_ = false; +} + +void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} +void BP1658CJ::write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); +} + +void BP1658CJ::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); +} + +void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); +} + +} // namespace bp1658cj +} // namespace esphome diff --git a/esphome/components/bp1658cj/bp1658cj.h b/esphome/components/bp1658cj/bp1658cj.h new file mode 100644 index 0000000000..778f49b3e9 --- /dev/null +++ b/esphome/components/bp1658cj/bp1658cj.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace bp1658cj { + +class BP1658CJ : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_max_power_color_channels(uint8_t max_power_color_channels) { + max_power_color_channels_ = max_power_color_channels; + } + void set_max_power_white_channels(uint8_t max_power_white_channels) { + max_power_white_channels_ = max_power_white_channels; + } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(BP1658CJ *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0x3FF); + this->parent_->set_channel_value_(this->channel_, amount); + } + + BP1658CJ *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint16_t value); + void write_bit_(bool value); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t max_power_color_channels_{4}; + uint8_t max_power_white_channels_{6}; + uint8_t update_channel_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace bp1658cj +} // namespace esphome diff --git a/esphome/components/bp1658cj/output.py b/esphome/components/bp1658cj/output.py new file mode 100644 index 0000000000..3b89518621 --- /dev/null +++ b/esphome/components/bp1658cj/output.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import BP1658CJ + +DEPENDENCIES = ["bp1658cj"] + +Channel = BP1658CJ.class_("Channel", output.FloatOutput) + +CONF_BP1658CJ_ID = "bp1658cj_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_BP1658CJ_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/bp5758d/__init__.py b/esphome/components/bp5758d/__init__.py new file mode 100644 index 0000000000..eeeab2a1bd --- /dev/null +++ b/esphome/components/bp5758d/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +AUTO_LOAD = ["output"] +bp5758d_ns = cg.esphome_ns.namespace("bp5758d") +BP5758D = bp5758d_ns.class_("BP5758D", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BP5758D), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp new file mode 100644 index 0000000000..111fd6b68e --- /dev/null +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -0,0 +1,147 @@ +#include "bp5758d.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bp5758d { + +static const char *const TAG = "bp5758d"; + +static const uint8_t BP5758D_MODEL_ID = 0b10000000; +static const uint8_t BP5758D_ADDR_STANDBY = 0b00000000; +// Note, channel start address seems ambiguous and mis-translated. +// Documentation states all are "invalid sleep" +// All 3 values appear to activate all 5 channels. Using the mapping +// from BP1658CJ ordering since it won't break anything. +static const uint8_t BP5758D_ADDR_START_3CH = 0b00010000; +static const uint8_t BP5758D_ADDR_START_2CH = 0b00100000; +static const uint8_t BP5758D_ADDR_START_5CH = 0b00110000; +static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111; + +void BP5758D::setup() { + ESP_LOGCONFIG(TAG, "Setting up BP5758D Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(false); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(false); + this->channel_current_.resize(5, 0); + this->pwm_amounts_.resize(5, 0); +} +void BP5758D::dump_config() { + ESP_LOGCONFIG(TAG, "BP5758D:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); +} + +void BP5758D::loop() { + if (!this->update_) + return; + + uint8_t data[17]; + if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Off / Sleep + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; + for (int i = 1; i < 16; i++) + data[i] = 0; + this->write_buffer_(data, 17); + } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { + // Only data on white channels + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_2CH; + data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT; + data[2] = 0; + data[3] = 0; + data[4] = 0; + data[5] = this->pwm_amounts_[3] > 0 ? correct_current_level_bits_(this->channel_current_[3]) : 0; + data[6] = this->pwm_amounts_[4] > 0 ? correct_current_level_bits_(this->channel_current_[4]) : 0; + for (int i = 7, j = 0; i <= 15; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 17); + } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Only data on RGB channels + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_3CH; + data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT; + data[2] = this->pwm_amounts_[0] > 0 ? correct_current_level_bits_(this->channel_current_[0]) : 0; + data[3] = this->pwm_amounts_[1] > 0 ? correct_current_level_bits_(this->channel_current_[1]) : 0; + data[4] = this->pwm_amounts_[2] > 0 ? correct_current_level_bits_(this->channel_current_[2]) : 0; + data[5] = 0; + data[6] = 0; + for (int i = 7, j = 0; i <= 15; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 17); + } else { + // All channels + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_5CH; + data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT; + data[2] = this->pwm_amounts_[0] > 0 ? correct_current_level_bits_(this->channel_current_[0]) : 0; + data[3] = this->pwm_amounts_[1] > 0 ? correct_current_level_bits_(this->channel_current_[1]) : 0; + data[4] = this->pwm_amounts_[2] > 0 ? correct_current_level_bits_(this->channel_current_[2]) : 0; + data[5] = this->pwm_amounts_[3] > 0 ? correct_current_level_bits_(this->channel_current_[3]) : 0; + data[6] = this->pwm_amounts_[4] > 0 ? correct_current_level_bits_(this->channel_current_[4]) : 0; + for (int i = 7, j = 0; i <= 15; i += 2, j++) { + data[i] = this->pwm_amounts_[j] & 0x1F; + data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F; + } + this->write_buffer_(data, 17); + } + + this->update_ = false; +} + +uint8_t BP5758D::correct_current_level_bits_(uint8_t current) { + // Anything below 64 uses normal bitmapping. + if (current < 64) { + return current; + } + + // Anything above 63 needs to be offset by +34 because the driver remaps bit 7 (normally 64) to 30. + // (no idea why(!) but it is documented) + // Example: + // integer 64 would normally put out 0b01000000 but here 0b01000000 = 30 whereas everything lower + // is normal, so we add 34 to the integer where + // integer 98 = 0b01100010 which is 30 (7th bit adjusted) + 34 (1st-6th bits). + return current + 34; +} + +void BP5758D::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} + +void BP5758D::set_channel_current_(uint8_t channel, uint8_t current) { this->channel_current_[channel] = current; } + +void BP5758D::write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); +} + +void BP5758D::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); +} + +void BP5758D::write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); +} + +} // namespace bp5758d +} // namespace esphome diff --git a/esphome/components/bp5758d/bp5758d.h b/esphome/components/bp5758d/bp5758d.h new file mode 100644 index 0000000000..cc7cc3d5f8 --- /dev/null +++ b/esphome/components/bp5758d/bp5758d.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace bp5758d { + +class BP5758D : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(BP5758D *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + void set_current(uint8_t current) { current_ = current; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0x3FF); + // We're enforcing channels start at 1 to mach OUT1-OUT5, we must adjust + // to our 0-based array internally here by subtracting 1. + this->parent_->set_channel_value_(this->channel_ - 1, amount); + this->parent_->set_channel_current_(this->channel_ - 1, this->current_); + } + + BP5758D *parent_; + uint8_t channel_; + uint8_t current_; + }; + + protected: + uint8_t correct_current_level_bits_(uint8_t current); + void set_channel_value_(uint8_t channel, uint16_t value); + void set_channel_current_(uint8_t channel, uint8_t current); + void write_bit_(bool value); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t update_channel_; + std::vector channel_current_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace bp5758d +} // namespace esphome diff --git a/esphome/components/bp5758d/output.py b/esphome/components/bp5758d/output.py new file mode 100644 index 0000000000..d0083fb33f --- /dev/null +++ b/esphome/components/bp5758d/output.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_CURRENT +from . import BP5758D + +DEPENDENCIES = ["bp5758d"] + +Channel = BP5758D.class_("Channel", output.FloatOutput) + +CONF_BP5758D_ID = "bp5758d_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_BP5758D_ID): cv.use_id(BP5758D), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=5), + cv.Optional(CONF_CURRENT, default=10): cv.int_range(min=0, max=90), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_BP5758D_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_current(config[CONF_CURRENT])) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8a3cd38444..eaa87afcb1 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -22,6 +22,8 @@ from esphome.const import ( CONF_MODE_STATE_TOPIC, CONF_ON_STATE, CONF_PRESET, + CONF_PRESET_COMMAND_TOPIC, + CONF_PRESET_STATE_TOPIC, CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, @@ -73,6 +75,7 @@ CLIMATE_FAN_MODES = { "MIDDLE": ClimateFanMode.CLIMATE_FAN_MIDDLE, "FOCUS": ClimateFanMode.CLIMATE_FAN_FOCUS, "DIFFUSE": ClimateFanMode.CLIMATE_FAN_DIFFUSE, + "QUIET": ClimateFanMode.CLIMATE_FAN_QUIET, } validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) @@ -142,6 +145,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_MODE_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -216,7 +225,12 @@ async def setup_climate_core_(var, config): cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC])) if CONF_MODE_STATE_TOPIC in config: cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC])) - + if CONF_PRESET_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_preset_command_topic(config[CONF_PRESET_COMMAND_TOPIC]) + ) + if CONF_PRESET_STATE_TOPIC in config: + cg.add(mqtt_.set_custom_preset_state_topic(config[CONF_PRESET_STATE_TOPIC])) if CONF_SWING_MODE_COMMAND_TOPIC in config: cg.add( mqtt_.set_custom_swing_mode_command_topic( diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 512dbdd6dd..e1611d2fa9 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -174,6 +174,8 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { this->set_fan_mode(CLIMATE_FAN_FOCUS); } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { this->set_fan_mode(CLIMATE_FAN_DIFFUSE); + } else if (str_equals_case_insensitive(fan_mode, "QUIET")) { + this->set_fan_mode(CLIMATE_FAN_QUIET); } else { if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { this->custom_fan_mode_ = fan_mode; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index e46159a750..794f45ccd6 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -62,6 +62,8 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { return LOG_STR("FOCUS"); case climate::CLIMATE_FAN_DIFFUSE: return LOG_STR("DIFFUSE"); + case climate::CLIMATE_FAN_QUIET: + return LOG_STR("QUIET"); default: return LOG_STR("UNKNOWN"); } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 139400a08a..c5245812c7 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -62,6 +62,8 @@ enum ClimateFanMode : uint8_t { CLIMATE_FAN_FOCUS = 7, /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, + /// The fan mode is set to Quiet + CLIMATE_FAN_QUIET = 9, }; /// Enum for all modes a climate swing can be in diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 3ec51bc3c2..9da9bb7374 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -28,7 +28,7 @@ namespace climate { * - supports action - if the climate device supports reporting the active * current action of the device with the action property. * - supports fan modes - optionally, if it has a fan which can be configured in different ways: - * - on, off, auto, high, medium, low, middle, focus, diffuse + * - on, off, auto, high, medium, low, middle, focus, diffuse, quiet * - supports swing modes - optionally, if it has a swing which can be configured in different ways: * - off, both, vertical, horizontal * diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 0eb65579f9..e1bd6a7f08 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -1,14 +1,17 @@ +import base64 +import secrets from pathlib import Path +from typing import Optional + import requests import esphome.codegen as cg import esphome.config_validation as cv +from esphome import git from esphome.components.packages import validate_source_shorthand -from esphome.const import CONF_WIFI, CONF_REF +from esphome.const import CONF_REF, CONF_WIFI from esphome.wizard import wizard_file from esphome.yaml_util import dump -from esphome import git - dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") @@ -66,7 +69,13 @@ async def to_code(config): def import_config( - path: str, name: str, project_name: str, import_url: str, network: str = CONF_WIFI + path: str, + name: str, + friendly_name: Optional[str], + project_name: str, + import_url: str, + network: str = CONF_WIFI, + encryption: bool = False, ) -> None: p = Path(path) @@ -74,14 +83,21 @@ def import_config( raise FileExistsError if project_name == "esphome.web": + 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", + "ssid": "!secret wifi_ssid", + "psk": "!secret wifi_password", + } + if encryption: + noise_psk = secrets.token_bytes(32) + key = base64.b64encode(noise_psk).decode() + kwargs["api_encryption_key"] = key + p.write_text( - wizard_file( - name=name, - platform="ESP32" if "esp32" in import_url else "ESP8266", - board="esp32dev" if "esp32" in import_url else "esp01_1m", - ssid="!secret wifi_ssid", - psk="!secret wifi_password", - ), + wizard_file(**kwargs), encoding="utf8", ) else: @@ -98,14 +114,21 @@ def import_config( p.write_text(req.text, encoding="utf8") else: + substitutions = {"name": name} + esphome_core = {"name": "${name}", "name_add_mac_suffix": False} + if friendly_name: + substitutions["friendly_name"] = friendly_name + esphome_core["friendly_name"] = "${friendly_name}" config = { - "substitutions": {"name": name}, + "substitutions": substitutions, "packages": {project_name: import_url}, - "esphome": { - "name": "${name}", - "name_add_mac_suffix": False, - }, + "esphome": esphome_core, } + if encryption: + noise_psk = secrets.token_bytes(32) + key = base64.b64encode(noise_psk).decode() + config["api"] = {"encryption": {"key": key}} + output = dump(config) if network == CONF_WIFI: diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 0cf48dd4ee..1ba80aabf5 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -111,6 +111,7 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE, + climate::CLIMATE_FAN_QUIET, }); traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); traits.set_supported_swing_modes({ diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 97c08dae24..9fe4137a14 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -15,6 +15,84 @@ static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); +void Rect::expand(int16_t horizontal, int16_t vertical) { + if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { + this->x = this->x - horizontal; + this->y = this->y - vertical; + this->w = this->w + (2 * horizontal); + this->h = this->h + (2 * vertical); + } +} + +void Rect::extend(Rect rect) { + if (!this->is_set()) { + this->x = rect.x; + this->y = rect.y; + this->w = rect.w; + this->h = rect.h; + } else { + if (this->x > rect.x) { + this->x = rect.x; + } + if (this->y > rect.y) { + this->y = rect.y; + } + if (this->x2() < rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() < rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} +void Rect::shrink(Rect rect) { + if (!this->inside(rect)) { + (*this) = Rect(); + } else { + if (this->x < rect.x) { + this->x = rect.x; + } + if (this->y < rect.y) { + this->y = rect.y; + } + if (this->x2() > rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() > rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} + +bool Rect::inside(int16_t x, int16_t y, bool absolute) { // NOLINT + if (!this->is_set()) { + return true; + } + if (absolute) { + return ((x >= 0) && (x <= this->w) && (y >= 0) && (y <= this->h)); + } else { + return ((x >= this->x) && (x <= this->x2()) && (y >= this->y) && (y <= this->y2())); + } +} + +bool Rect::inside(Rect rect, bool absolute) { + if (!this->is_set() || !rect.is_set()) { + return true; + } + if (absolute) { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); + } else { + return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } +} + +void Rect::info(const std::string &prefix) { + if (this->is_set()) { + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d]", prefix.c_str(), this->x, this->y, this->w, this->h); + } else + ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); +} + void DisplayBuffer::init_internal_(uint32_t buffer_length) { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->buffer_ = allocator.allocate(buffer_length); @@ -24,6 +102,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } + void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { @@ -50,6 +129,9 @@ int DisplayBuffer::get_height() { } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; // NOLINT + switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -368,6 +450,10 @@ void DisplayBuffer::do_update_() { } else if (this->writer_.has_value()) { (*this->writer_)(*this); } + // remove all not ended clipping regions + while (is_clipping()) { + end_clipping(); + } } void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) @@ -392,6 +478,41 @@ void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time: } #endif +void DisplayBuffer::start_clipping(Rect rect) { + if (!this->clipping_rectangle_.empty()) { + Rect r = this->clipping_rectangle_.back(); + rect.shrink(r); + } + this->clipping_rectangle_.push_back(rect); +} +void DisplayBuffer::end_clipping() { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "clear: Clipping is not set."); + } else { + this->clipping_rectangle_.pop_back(); + } +} +void DisplayBuffer::extend_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().extend(add_rect); + } +} +void DisplayBuffer::shrink_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().shrink(add_rect); + } +} +Rect DisplayBuffer::get_clipping() { + if (this->clipping_rectangle_.empty()) { + return Rect(); + } else { + return this->clipping_rectangle_.back(); + } +} bool Glyph::get_pixel(int x, int y) const { const int x_data = x - this->glyph_data_->offset_x; const int y_data = y - this->glyph_data_->offset_y; @@ -452,7 +573,7 @@ int Font::match_next_glyph(const char *str, int *match_length) { } void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { *baseline = this->baseline_; - *height = this->bottom_; + *height = this->height_; int i = 0; int min_x = 0; bool has_char = false; @@ -483,7 +604,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in *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 bottom) : baseline_(baseline), bottom_(bottom) { +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); } @@ -527,6 +648,7 @@ int Image::get_height() const { return this->height_; } ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height, ImageType type) : width_(width), height_(height), type_(type), data_start_(data_start) {} +int Image::get_current_frame() const { return 0; } bool Animation::get_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 41052b3ffd..3763da8041 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -4,7 +4,6 @@ #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "display_color_utils.h" - #include #include @@ -100,6 +99,32 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; +static const int16_t VALUE_NO_SET = 32766; + +class Rect { + public: + int16_t x; ///< X coordinate of corner + int16_t y; ///< Y coordinate of corner + int16_t w; ///< Width of region + int16_t h; ///< Height of region + + Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT + inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} + inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner + inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner + + inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } + + void expand(int16_t horizontal, int16_t vertical); + + void extend(Rect rect); + void shrink(Rect rect); + + bool inside(Rect rect, bool absolute = false); + bool inside(int16_t x, int16_t y, bool absolute = false); + void info(const std::string &prefix = "rect info:"); +}; + class Font; class Image; class DisplayBuffer; @@ -126,6 +151,7 @@ class DisplayBuffer { int get_width(); /// Get the height of the image in pixels with rotation applied. int get_height(); + /// Set a single pixel at the specified coordinates to the given color. void draw_pixel_at(int x, int y, Color color = COLOR_ON); @@ -374,6 +400,49 @@ class DisplayBuffer { */ virtual DisplayType get_display_type() = 0; + /** Set the clipping rectangle for further drawing + * + * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + * + * return true if success, false if error + */ + void start_clipping(Rect rect); + void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + start_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Add a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void extend_clipping(Rect rect); + void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + this->extend_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** substract a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void shrink_clipping(Rect rect); + void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + this->shrink_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Reset the invalidation region + */ + void end_clipping(); + + /** Get the current the clipping rectangle + * + * return rect for active clipping region + */ + Rect get_clipping(); + + bool is_clipping() const { return !this->clipping_rectangle_.empty(); } + protected: void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); @@ -390,6 +459,7 @@ class DisplayBuffer { DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; bool auto_clear_enabled_{true}; + std::vector clipping_rectangle_; }; class DisplayPage { @@ -448,18 +518,20 @@ class Font { * @param baseline The y-offset from the top of the text to the baseline. * @param bottom The y-offset from the top of the text to the bottom (i.e. height). */ - Font(const GlyphData *data, int data_nr, int baseline, int bottom); + Font(const GlyphData *data, int data_nr, int baseline, int height); int match_next_glyph(const char *str, int *match_length); void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); + inline int get_baseline() { return this->baseline_; } + inline int get_height() { return this->height_; } const std::vector &get_glyphs() const; protected: std::vector glyphs_; int baseline_; - int bottom_; + int height_; }; class Image { @@ -473,6 +545,8 @@ class Image { int get_height() const; ImageType get_type() const; + virtual int get_current_frame() const; + protected: int width_; int height_; @@ -489,7 +563,7 @@ class Animation : public Image { Color get_grayscale_pixel(int x, int y) const override; int get_animation_frame_count() const; - int get_current_frame() const; + int get_current_frame() const override; void next_frame(); void prev_frame(); diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index eb66737fdb..d7326cdc65 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_ON_VALUE, CONF_COMMAND, + CONF_CUSTOM, CONF_NUMBER, CONF_FORMAT, CONF_MODE, @@ -32,7 +33,6 @@ CONF_BACK = "back" CONF_TEXT = "text" CONF_SELECT = "select" CONF_SWITCH = "switch" -CONF_CUSTOM = "custom" CONF_ITEMS = "items" CONF_ON_TEXT = "on_text" CONF_OFF_TEXT = "off_text" diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 284733cca6..f4f8305ba6 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CODEOWNERS = ["@glmnet", "@zuidwijk"] +MULTI_CONF = True + DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor", "text_sensor"] diff --git a/esphome/components/ee895/__init__.py b/esphome/components/ee895/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp new file mode 100644 index 0000000000..a7186ffbbc --- /dev/null +++ b/esphome/components/ee895/ee895.cpp @@ -0,0 +1,115 @@ +#include "ee895.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ee895 { + +static const char *const TAG = "ee895"; + +static const uint16_t CRC16_ONEWIRE_START = 0xFFFF; +static const uint8_t FUNCTION_CODE_READ = 0x03; +static const uint16_t SERIAL_NUMBER = 0x0000; +static const uint16_t TEMPERATURE_ADDRESS = 0x03EA; +static const uint16_t CO2_ADDRESS = 0x0424; +static const uint16_t PRESSURE_ADDRESS = 0x04B0; + +void EE895Component::setup() { + uint16_t crc16_check = 0; + ESP_LOGCONFIG(TAG, "Setting up EE895..."); + write_command_(SERIAL_NUMBER, 8); + uint8_t serial_number[20]; + this->read(serial_number, 20); + + crc16_check = (serial_number[19] << 8) + serial_number[18]; + if (crc16_check != calc_crc16_(serial_number, 19)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str()); +} + +void EE895Component::dump_config() { + ESP_LOGCONFIG(TAG, "EE895:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with EE895 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); +} + +float EE895Component::get_setup_priority() const { return setup_priority::DATA; } + +void EE895Component::update() { + write_command_(TEMPERATURE_ADDRESS, 2); + this->set_timeout(50, [this]() { + float temperature = read_float_(); + + write_command_(CO2_ADDRESS, 2); + float co2 = read_float_(); + + write_command_(PRESSURE_ADDRESS, 2); + float pressure = read_float_(); + ESP_LOGD(TAG, "Got temperature=%.1f°C co2=%.0fppm pressure=%.1f%mbar", temperature, co2, pressure); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + }); +} + +void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { + uint8_t address[7]; + uint16_t crc16 = 0; + address[0] = FUNCTION_CODE_READ; + address[1] = (addr >> 8) & 0xFF; + address[2] = addr & 0xFF; + address[3] = (reg_cnt >> 8) & 0xFF; + address[4] = reg_cnt & 0xFF; + crc16 = calc_crc16_(address, 6); + address[5] = crc16 & 0xFF; + address[6] = (crc16 >> 8) & 0xFF; + this->write(address, 7, true); +} + +float EE895Component::read_float_() { + uint16_t crc16_check = 0; + uint8_t i2c_response[8]; + this->read(i2c_response, 8); + crc16_check = (i2c_response[7] << 8) + i2c_response[6]; + if (crc16_check != calc_crc16_(i2c_response, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return 0; + } + uint32_t x = encode_uint32(i2c_response[4], i2c_response[5], i2c_response[2], i2c_response[3]); + float value; + memcpy(&value, &x, sizeof(value)); // convert uin32_t IEEE-754 format to float + return value; +} + +uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { + uint8_t crc_check_buf[22]; + for (int i = 0; i < len; i++) { + crc_check_buf[i + 1] = buf[i]; + } + crc_check_buf[0] = this->address_; + return crc16(crc_check_buf, len); +} +} // namespace ee895 +} // namespace esphome diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h new file mode 100644 index 0000000000..83bd7c6e82 --- /dev/null +++ b/esphome/components/ee895/ee895.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ee895 { + +/// This class implements support for the ee895 of temperature i2c sensors. +class EE895Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + void write_command_(uint16_t addr, uint16_t reg_cnt); + float read_float_(); + uint16_t calc_crc16_(const uint8_t buf[], uint8_t len); + sensor::Sensor *co2_sensor_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace ee895 +} // namespace esphome diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py new file mode 100644 index 0000000000..d06f9ca02f --- /dev/null +++ b/esphome/components/ee895/sensor.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + CONF_CO2, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_HECTOPASCAL, + UNIT_CELSIUS, + ICON_MOLECULE_CO2, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +ee895_ns = cg.esphome_ns.namespace("ee895") +EE895Component = ee895_ns.class_("EE895Component", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EE895Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5F)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3989b62842..f30fa9a7b2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -356,9 +356,14 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_MAC_CRC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_MAC_CRC") - add_idf_sdkconfig_option( - "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) + if (framework_ver.major, framework_ver.minor) >= (4, 4): + add_idf_sdkconfig_option( + "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) + else: + add_idf_sdkconfig_option( + "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) cg.add_define( "USE_ESP_IDF_VERSION_CODE", diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index b97a5e0457..7848d1d552 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -123,11 +123,8 @@ def validate_gpio_pin(value): def validate_supports(value): mode = value[CONF_MODE] - is_input = mode[CONF_INPUT] is_output = mode[CONF_OUTPUT] is_open_drain = mode[CONF_OPEN_DRAIN] - is_pullup = mode[CONF_PULLUP] - is_pulldown = mode[CONF_PULLDOWN] variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: raise cv.Invalid(f"Unsupported ESP32 variant {variant}") @@ -138,26 +135,6 @@ def validate_supports(value): ) value = _esp32_validations[variant].usage_validation(value) - if CORE.using_arduino: - # (input, output, open_drain, pullup, pulldown) - supported_modes = { - # INPUT - (True, False, False, False, False), - # OUTPUT - (False, True, False, False, False), - # INPUT_PULLUP - (True, False, False, True, False), - # INPUT_PULLDOWN - (True, False, False, False, True), - # OUTPUT_OPEN_DRAIN - (False, True, True, False, False), - } - key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown) - if key not in supported_modes: - raise cv.Invalid( - "This pin mode is not supported on ESP32 for arduino frameworks", - [CONF_MODE], - ) return value diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index c6bb296cdc..f59d944dcf 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -6,13 +6,21 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] + +CONF_BLE_ID = "ble_id" + +NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2] NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) +GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") +GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") +GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") + CONFIG_SCHEMA = cv.Schema( { diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ecd591d169..160084b913 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -4,13 +4,13 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -#include -#include -#include #include -#include -#include +#include #include +#include +#include +#include +#include #ifdef USE_ARDUINO #include @@ -31,24 +31,17 @@ void ESP32BLE::setup() { return; } +#ifdef USE_ESP32_BLE_SERVER this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) this->advertising_->set_scan_response(true); this->advertising_->set_min_preferred_interval(0x06); this->advertising_->start(); +#endif // USE_ESP32_BLE_SERVER ESP_LOGD(TAG, "BLE setup complete"); } -void ESP32BLE::mark_failed() { - Component::mark_failed(); -#ifdef USE_ESP32_BLE_SERVER - if (this->server_ != nullptr) { - this->server_->mark_failed(); - } -#endif -} - bool ESP32BLE::ble_setup_() { esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { @@ -100,13 +93,16 @@ bool ESP32BLE::ble_setup_() { ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); return false; } - err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; + + if (!this->gap_event_handlers_.empty()) { + err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; + } } - if (this->has_server()) { + if (!this->gatts_event_handlers_.empty()) { err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); @@ -114,7 +110,7 @@ bool ESP32BLE::ble_setup_() { } } - if (this->has_client()) { + if (!this->gattc_event_handlers_.empty()) { err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); @@ -158,6 +154,10 @@ void ESP32BLE::loop() { this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, &ble_event->event_.gatts.gatts_param); break; + case BLEEvent::GATTC: + this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + break; case BLEEvent::GAP: this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); break; @@ -176,9 +176,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); - switch (event) { - default: - break; + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(event, param); } } @@ -191,14 +190,23 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); -#ifdef USE_ESP32_BLE_SERVER - this->server_->gatts_event_handler(event, gatts_if, param); -#endif + for (auto *gatts_handler : this->gatts_event_handlers_) { + gatts_handler->gatts_event_handler(event, gatts_if, param); + } } +void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) + global_ble->ble_events_.push(new_event); +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - // this->client_->gattc_event_handler(event, gattc_if, param); + ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); + for (auto *gattc_handler : this->gattc_event_handlers_) { + gattc_handler->gattc_event_handler(event, gattc_if, param); + } } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 0477dee070..5970b43688 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -5,17 +5,16 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "queue.h" -#ifdef USE_ESP32_BLE_SERVER -#include "esphome/components/esp32_ble_server/ble_server.h" -#endif +#include "queue.h" +#include "ble_event.h" #ifdef USE_ESP32 #include #include #include + namespace esphome { namespace esp32_ble { @@ -26,28 +25,36 @@ typedef struct { uint16_t mtu; } conn_status_t; +class GAPEventHandler { + public: + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; +}; + +class GATTcEventHandler { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; +}; + +class GATTsEventHandler { + public: + virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) = 0; +}; + class ESP32BLE : public Component { public: void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; - void mark_failed() override; - - bool has_server() { -#ifdef USE_ESP32_BLE_SERVER - return this->server_ != nullptr; -#else - return false; -#endif - } - bool has_client() { return false; } BLEAdvertising *get_advertising() { return this->advertising_; } -#ifdef USE_ESP32_BLE_SERVER - void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; } -#endif + void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } + void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + protected: static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); @@ -59,9 +66,10 @@ class ESP32BLE : public Component { bool ble_setup_(); -#ifdef USE_ESP32_BLE_SERVER - esp32_ble_server::BLEServer *server_{nullptr}; -#endif + std::vector gap_event_handlers_; + std::vector gattc_event_handlers_; + std::vector gatts_event_handlers_; + Queue ble_events_; BLEAdvertising *advertising_; }; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble/ble_event.h similarity index 50% rename from esphome/components/esp32_ble_tracker/queue.h rename to esphome/components/esp32_ble/ble_event.h index f1dcc337e8..1cf63b2fab 100644 --- a/esphome/components/esp32_ble_tracker/queue.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -1,69 +1,23 @@ #pragma once #ifdef USE_ESP32 -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" -#include -#include -#include #include #include #include -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal with various locking strategies, all incoming GAP and GATT - * events will simply be placed on a semaphore guarded queue. The next time the - * component runs loop(), these events are popped off the queue and handed at - * this safer time. - */ +#include namespace esphome { -namespace esp32_ble_tracker { - -template class Queue { - public: - Queue() { m_ = xSemaphoreCreateMutex(); } - - void push(T *element) { - if (element == nullptr) - return; - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - q_.push(element); - xSemaphoreGive(m_); - } - } - - T *pop() { - T *element = nullptr; - - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - if (!q_.empty()) { - element = q_.front(); - q_.pop(); - } - xSemaphoreGive(m_); - } - return element; - } - - protected: - std::queue q_; - SemaphoreHandle_t m_; -}; - -// Received GAP and GATTC events are only queued, and get processed in the main loop(). +namespace esp32_ble { +// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // This class stores each event in a single type. class BLEEvent { public: BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { this->event_.gap.gap_event = e; memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = 0; + this->type_ = GAP; }; BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { @@ -84,26 +38,57 @@ class BLEEvent { default: break; } - this->type_ = 1; + this->type_ = GATTC; + }; + + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); + // Need to also make a copy of relevant event data. + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->data.assign(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param.write.value = this->data.data(); + break; + default: + break; + } + this->type_ = GATTS; }; union { - struct gap_event { // NOLINT(readability-identifier-naming) + // NOLINTNEXTLINE(readability-identifier-naming) + struct gap_event { esp_gap_ble_cb_event_t gap_event; esp_ble_gap_cb_param_t gap_param; } gap; - struct gattc_event { // NOLINT(readability-identifier-naming) + // NOLINTNEXTLINE(readability-identifier-naming) + struct gattc_event { esp_gattc_cb_event_t gattc_event; esp_gatt_if_t gattc_if; esp_ble_gattc_cb_param_t gattc_param; } gattc; + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gatts_event { + esp_gatts_cb_event_t gatts_event; + esp_gatt_if_t gatts_if; + esp_ble_gatts_cb_param_t gatts_param; + } gatts; } event_; + std::vector data{}; - uint8_t type_; // 0=gap 1=gattc + // NOLINTNEXTLINE(readability-identifier-naming) + enum ble_event_t : uint8_t { + GAP, + GATTC, + GATTS, + } type_; }; -} // namespace esp32_ble_tracker +} // namespace esp32_ble } // namespace esphome #endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index 8556aa87df..a50d3dbd42 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -27,8 +27,7 @@ ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ESPBTUUID ret; ret.uuid_.len = ESP_UUID_LEN_128; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = data[i]; + memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128); return ret; } ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { @@ -91,10 +90,13 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { ESPBTUUID ret; ret.uuid_.len = uuid.len; - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + if (uuid.len == ESP_UUID_LEN_16) { + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + } else if (uuid.len == ESP_UUID_LEN_32) { + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + } else if (uuid.len == ESP_UUID_LEN_128) { + memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); + } return ret; } ESPBTUUID ESPBTUUID::as_128bit() const { @@ -158,30 +160,26 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { } return false; } -esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } -std::string ESPBTUUID::to_string() { - char sbuf[64]; +esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } +std::string ESPBTUUID::to_string() const { switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - break; + return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); case ESP_UUID_LEN_32: - sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), - (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); - break; + return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, + (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), + this->uuid_.uuid.uuid32 & 0xff); default: case ESP_UUID_LEN_128: - char *bpos = sbuf; + std::string buf; for (int8_t i = 15; i >= 0; i--) { - sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); - bpos += 2; + buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); if (i == 6 || i == 8 || i == 10 || i == 12) - sprintf(bpos++, "-"); + buf += "-"; } - sbuf[47] = '\0'; - break; + return buf; } - return sbuf; + return ""; } } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index f953f9fede..790a57c59d 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -32,9 +32,9 @@ class ESPBTUUID { bool operator==(const ESPBTUUID &uuid) const; bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - esp_bt_uuid_t get_uuid(); + esp_bt_uuid_t get_uuid() const; - std::string to_string(); + std::string to_string() const; protected: esp_bt_uuid_t uuid_; diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 8d05eca058..5b31b97ae2 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -2,16 +2,9 @@ #ifdef USE_ESP32 -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" - -#include #include -#include +#include -#include -#include -#include #include #include @@ -57,84 +50,6 @@ template class Queue { SemaphoreHandle_t m_; }; -// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). -// This class stores each event in a single type. -class BLEEvent { - public: - BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->event_.gap.gap_event = e; - memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = GAP; - }; - - BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); - // Need to also make a copy of notify event data. - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; - break; - default: - break; - } - this->type_ = GATTC; - }; - - BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); - // Need to also make a copy of write data. - switch (e) { - case ESP_GATTS_WRITE_EVT: - memcpy(this->event_.gatts.data, p->write.value, p->write.len); - this->event_.gatts.gatts_param.write.value = this->event_.gatts.data; - break; - default: - break; - } - this->type_ = GATTS; - }; - - union { - // NOLINTNEXTLINE(readability-identifier-naming) - struct gap_event { - esp_gap_ble_cb_event_t gap_event; - esp_ble_gap_cb_param_t gap_param; - } gap; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t gattc_param; - uint8_t data[64]; - } gattc; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t gatts_param; - uint8_t data[64]; - } gatts; - } event_; - // NOLINTNEXTLINE(readability-identifier-naming) - enum ble_event_t : uint8_t { - GAP, - GATTC, - GATTS, - } type_; -}; - } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2337a5fe93..2793a74c5a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -40,7 +40,7 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { - if (device.address_uint64() != this->address_) + if (this->address_ == 0 || device.address_uint64() != this->address_) return false; if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) return false; @@ -138,6 +138,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->address_str_.c_str(), ret); } if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); this->set_state(espbt::ClientState::CONNECTED); this->state_ = espbt::ClientState::ESTABLISHED; break; @@ -189,6 +190,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_.c_str(), svc->start_handle, svc->end_handle); } + ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); this->set_state(espbt::ClientState::CONNECTED); this->state_ = espbt::ClientState::ESTABLISHED; break; diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 2fcc5c7743..0ddfa62c1b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -7,21 +7,25 @@ from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["esp32"] CONF_MANUFACTURER = "manufacturer" -CONF_BLE_ID = "ble_id" esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") -BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component) +BLEServer = esp32_ble_server_ns.class_( + "BLEServer", + cg.Component, + esp32_ble.GATTsEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), - cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, cv.Optional(CONF_MODEL): cv.string, } @@ -29,16 +33,18 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): - parent = await cg.get_variable(config[CONF_BLE_ID]) var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gatts_event_handler(var)) + cg.add(var.set_parent(parent)) + cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) if CONF_MODEL in config: cg.add(var.set_model(config[CONF_MODEL])) cg.add_define("USE_ESP32_BLE_SERVER") - cg.add(parent.set_server(var)) - if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 15bea07021..7cbf40c076 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -25,7 +25,8 @@ static const uint16_t VERSION_UUID = 0x2A26; static const uint16_t MANUFACTURER_UUID = 0x2A29; void BLEServer::setup() { - if (this->is_failed()) { + if (this->parent_->is_failed()) { + this->mark_failed(); ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE"); return; } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index f82e854090..ac759f2dcd 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -3,6 +3,7 @@ #include "ble_service.h" #include "ble_characteristic.h" +#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_advertising.h" #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/queue.h" @@ -32,7 +33,7 @@ class BLEServiceComponent { virtual void stop(); }; -class BLEServer : public Component { +class BLEServer : public Component, public GATTsEventHandler, public Parented { public: void setup() override; void loop() override; @@ -55,7 +56,8 @@ class BLEServer : public Component { uint32_t get_connected_client_count() { return this->connected_clients_; } const std::map &get_clients() { return this->clients_; } - void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) override; void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index c20491e701..c5af845987 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -15,10 +15,12 @@ from esphome.const import ( CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, ) +from esphome.components import esp32_ble from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components import esp32_ble +AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] CONF_ESP32_BLE_ID = "esp32_ble_id" @@ -27,7 +29,13 @@ CONF_WINDOW = "window" CONF_CONTINUOUS = "continuous" CONF_ON_SCAN_END = "on_scan_end" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") -ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESP32BLETracker = esp32_ble_tracker_ns.class_( + "ESP32BLETracker", + cg.Component, + esp32_ble.GAPEventHandler, + esp32_ble.GATTcEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") @@ -138,6 +146,7 @@ def as_reversed_hex_array(value): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLETracker), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( cv.Schema( { @@ -200,6 +209,12 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gap_event_handler(var)) + cg.add(parent.register_gattc_event_handler(var)) + cg.add(var.set_parent(parent)) + params = config[CONF_SCAN_PARAMETERS] cg.add(var.set_scan_duration(params[CONF_DURATION])) cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) @@ -248,6 +263,7 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + cg.add_define("USE_ESP32_BLE_CLIENT") ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b1d469025b..6b0f4dc897 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -7,14 +7,14 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include -#include #include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include #ifdef USE_OTA #include "esphome/components/ota/ota_component.h" @@ -45,17 +45,19 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { return u; } -float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; } +float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { + if (this->parent_->is_failed()) { + this->mark_failed(); + ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); + return; + } + global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); this->scan_end_lock_ = xSemaphoreCreateMutex(); this->scanner_idle_ = true; - if (!ESP32BLETracker::ble_setup()) { - this->mark_failed(); - return; - } #ifdef USE_OTA ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) { @@ -75,18 +77,6 @@ void ESP32BLETracker::setup() { } void ESP32BLETracker::loop() { - BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { - if (ble_event->type_) { - this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, - &ble_event->event_.gattc.gattc_param); - } else { - this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); - } - delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) - ble_event = this->ble_events_.pop(); - } - int connecting = 0; int discovered = 0; int searching = 0; @@ -238,85 +228,6 @@ void ESP32BLETracker::stop_scan() { this->cancel_timeout("scan"); } -bool ESP32BLETracker::ble_setup() { - // Initialize non-volatile storage for the bluetooth controller - esp_err_t err = nvs_flash_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); - return false; - } - -#ifdef USE_ARDUINO - if (!btStart()) { - ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); - return false; - } -#else - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - // start bt controller - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { - esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); - err = esp_bt_controller_init(&cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); - return false; - } - while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) - ; - } - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { - err = esp_bt_controller_enable(ESP_BT_MODE_BLE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err)); - return false; - } - } - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - ESP_LOGE(TAG, "esp bt controller enable failed"); - return false; - } - } -#endif - - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - - err = esp_bluedroid_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); - return false; - } - err = esp_bluedroid_enable(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); - return false; - } - err = esp_ble_gap_register_callback(ESP32BLETracker::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; - } - err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); - return false; - } - - // Empty name - esp_ble_gap_set_device_name(""); - - esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); - return false; - } - - // BLE takes some time to be fully set up, 200ms should be more than enough - delay(200); // NOLINT - - return true; -} - void ESP32BLETracker::start_scan_(bool first) { // The lock must be held when calling this function. if (xSemaphoreTake(this->scan_end_lock_, 0L)) { @@ -369,11 +280,6 @@ void ESP32BLETracker::register_client(ESPBTClient *client) { } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *gap_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) - global_esp32_ble_tracker->ble_events_.push(gap_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) - -void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: this->gap_scan_result_(param->scan_rst); @@ -428,204 +334,11 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) - global_esp32_ble_tracker->ble_events_.push(gattc_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) - -void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { for (auto *client : this->clients_) { client->gattc_event_handler(event, gattc_if, param); } } -ESPBTUUID::ESPBTUUID() : uuid_() {} -ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_128; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = data[i]; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { - ESPBTUUID ret; - if (data.length() == 4) { - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = 0; - for (int i = 0; i < data.length();) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4; - i += 2; - } - } else if (data.length() == 8) { - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = 0; - for (int i = 0; i < data.length();) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4; - i += 2; - } - } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be - // investigated (lack of time) - ret.uuid_.len = ESP_UUID_LEN_128; - memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); - } else if (data.length() == 36) { - // If the length of the string is 36 bytes then we will assume it is a long hex string in - // UUID format. - ret.uuid_.len = ESP_UUID_LEN_128; - int n = 0; - for (int i = 0; i < data.length();) { - if (data.c_str()[i] == '-') - i++; - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); - i += 2; - } - } else { - ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); - } - return ret; -} -ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = uuid.len; - if (uuid.len == ESP_UUID_LEN_16) { - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - } else if (uuid.len == ESP_UUID_LEN_32) { - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - } else if (uuid.len == ESP_UUID_LEN_128) { - memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); - } - return ret; -} -ESPBTUUID ESPBTUUID::as_128bit() const { - if (this->uuid_.len == ESP_UUID_LEN_128) { - return *this; - } - uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - uint32_t uuid32; - if (this->uuid_.len == ESP_UUID_LEN_32) { - uuid32 = this->uuid_.uuid.uuid32; - } else { - uuid32 = this->uuid_.uuid.uuid16; - } - for (uint8_t i = 0; i < this->uuid_.len; i++) { - data[12 + i] = ((uuid32 >> i * 8) & 0xFF); - } - return ESPBTUUID::from_raw(data); -} -bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { - if (this->uuid_.len == ESP_UUID_LEN_16) { - return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; - } else if (this->uuid_.len == ESP_UUID_LEN_32) { - for (uint8_t i = 0; i < 3; i++) { - bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; - bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; - if (a && b) - return true; - } - } else { - for (uint8_t i = 0; i < 15; i++) { - if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) - return true; - } - } - return false; -} -bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { - if (this->uuid_.len == uuid.uuid_.len) { - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { - return true; - } - break; - case ESP_UUID_LEN_32: - if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { - return true; - } - break; - case ESP_UUID_LEN_128: - for (int i = 0; i < ESP_UUID_LEN_128; i++) { - if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { - return false; - } - } - return true; - break; - } - } else { - return this->as_128bit() == uuid.as_128bit(); - } - return false; -} -esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } -std::string ESPBTUUID::to_string() const { - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - case ESP_UUID_LEN_32: - return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, - (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), - this->uuid_.uuid.uuid32 & 0xff); - default: - case ESP_UUID_LEN_128: - std::string buf; - for (int8_t i = 15; i >= 0; i--) { - buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); - if (i == 6 || i == 8 || i == 10 || i == 12) - buf += "-"; - } - return buf; - } - return ""; -} - -uint64_t ESPBTUUID::get_128bit_high() const { - esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); - return ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); -} -uint64_t ESPBTUUID::get_128bit_low() const { - esp_bt_uuid_t uuid = this->as_128bit().get_uuid(); - return ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); -} - ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { if (!data.uuid.contains(0x4C, 0x00)) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index e6f7829353..d1f72cf78d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -1,9 +1,8 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "queue.h" #include #include @@ -15,40 +14,16 @@ #include #include +#include +#include + +#include "esphome/components/esp32_ble/ble.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + namespace esphome { namespace esp32_ble_tracker { -class ESPBTUUID { - public: - ESPBTUUID(); - - static ESPBTUUID from_uint16(uint16_t uuid); - - static ESPBTUUID from_uint32(uint32_t uuid); - - static ESPBTUUID from_raw(const uint8_t *data); - - static ESPBTUUID from_raw(const std::string &data); - - static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); - - ESPBTUUID as_128bit() const; - - bool contains(uint8_t data1, uint8_t data2) const; - - bool operator==(const ESPBTUUID &uuid) const; - bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - - esp_bt_uuid_t get_uuid() const; - - std::string to_string() const; - - uint64_t get_128bit_high() const; - uint64_t get_128bit_low() const; - - protected: - esp_bt_uuid_t uuid_; -}; +using namespace esp32_ble; using adv_data_t = std::vector; @@ -191,7 +166,7 @@ class ESPBTClient : public ESPBTDeviceListener { ClientState state_; }; -class ESP32BLETracker : public Component { +class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEventHandler, public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } @@ -218,16 +193,15 @@ class ESP32BLETracker : public Component { void start_scan(); void stop_scan(); + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + protected: - /// The FreeRTOS task managing the bluetooth interface. - static bool ble_setup(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); /// Called when a scan ends void end_of_scan_(); - /// Callback that will handle all GAP events and redistribute them to other callbacks. - static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. @@ -238,9 +212,6 @@ class ESP32BLETracker : public Component { void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); int app_id_; - /// Callback that will handle all GATTC events and redistribute them to other callbacks. - static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); - void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; @@ -263,8 +234,6 @@ class ESP32BLETracker : public Component { esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; - - Queue ble_events_; }; // NOLINTNEXTLINE diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 753b6ed9da..b3abbd5c13 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -156,7 +156,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1e50418e01..7170a6dabf 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] CONF_AUTHORIZED_DURATION = "authorized_duration" diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 5ff4b0827d..85013c006b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -195,7 +195,7 @@ void ESP32ImprovComponent::send_response_(std::vector &response) { } void ESP32ImprovComponent::start() { - if (this->state_ != improv::STATE_STOPPED) + if (this->should_start_ || this->state_ != improv::STATE_STOPPED) return; ESP_LOGD(TAG, "Setting Improv to start"); diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index b3614d8fcf..a0f8b557b0 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -33,6 +33,7 @@ ETHERNET_TYPES = { "RTL8201": EthernetType.ETHERNET_TYPE_RTL8201, "DP83848": EthernetType.ETHERNET_TYPE_DP83848, "IP101": EthernetType.ETHERNET_TYPE_IP101, + "JL1101": EthernetType.ETHERNET_TYPE_JL1101, } emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c new file mode 100644 index 0000000000..6011795033 --- /dev/null +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -0,0 +1,339 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef USE_ESP32 + +#include +#include +#include +#include "esp_log.h" +#include "esp_eth.h" +#include "eth_phy_regs_struct.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_rom_gpio.h" +#include "esp_rom_sys.h" + +static const char *TAG = "jl1101"; +#define PHY_CHECK(a, str, goto_tag, ...) \ + do { \ + if (!(a)) { \ + ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + goto goto_tag; \ + } \ + } while (0) + +/***************Vendor Specific Register***************/ + +/** + * @brief PSR(Page Select Register) + * + */ +typedef union { + struct { + uint16_t page_select : 8; /* Select register page, default is 0 */ + uint16_t reserved : 8; /* Reserved */ + }; + uint16_t val; +} psr_reg_t; +#define ETH_PHY_PSR_REG_ADDR (0x1F) + +typedef struct { + esp_eth_phy_t parent; + esp_eth_mediator_t *eth; + int addr; + uint32_t reset_timeout_ms; + uint32_t autonego_timeout_ms; + eth_link_t link_status; + int reset_gpio_num; +} phy_jl1101_t; + +static esp_err_t jl1101_page_select(phy_jl1101_t *jl1101, uint32_t page) { + esp_eth_mediator_t *eth = jl1101->eth; + psr_reg_t psr = {.page_select = page}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_PSR_REG_ADDR, psr.val) == ESP_OK, "write PSR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_update_link_duplex_speed(phy_jl1101_t *jl1101) { + esp_eth_mediator_t *eth = jl1101->eth; + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + bmcr_reg_t bmcr; + bmsr_reg_t bmsr; + uint32_t peer_pause_ability = false; + anlpar_reg_t anlpar; + PHY_CHECK(jl1101_page_select(jl1101, 0) == ESP_OK, "select page 0 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)) == ESP_OK, + "read ANLPAR failed", err); + eth_link_t link = bmsr.link_status ? ETH_LINK_UP : ETH_LINK_DOWN; + /* check if link status changed */ + if (jl1101->link_status != link) { + /* when link up, read negotiation result */ + if (link == ETH_LINK_UP) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.speed_select) { + speed = ETH_SPEED_100M; + } else { + speed = ETH_SPEED_10M; + } + if (bmcr.duplex_mode) { + duplex = ETH_DUPLEX_FULL; + } else { + duplex = ETH_DUPLEX_HALF; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *) speed) == ESP_OK, "change speed failed", err); + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *) duplex) == ESP_OK, "change duplex failed", err); + /* if we're in duplex mode, and peer has the flow control ability */ + if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { + peer_pause_ability = 1; + } else { + peer_pause_ability = 0; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *) peer_pause_ability) == ESP_OK, + "change pause ability failed", err); + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_LINK, (void *) link) == ESP_OK, "change link failed", err); + jl1101->link_status = link; + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_mediator(esp_eth_phy_t *phy, esp_eth_mediator_t *eth) { + PHY_CHECK(eth, "can't set mediator to null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->eth = eth; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_get_link(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + /* Updata information about link, speed, duplex */ + PHY_CHECK(jl1101_update_link_duplex_speed(jl1101) == ESP_OK, "update link duplex speed failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->link_status = ETH_LINK_DOWN; + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr = {.reset = 1}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for reset complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 50; to++) { + vTaskDelay(pdMS_TO_TICKS(50)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!bmcr.reset) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 50, "reset timeout", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + if (jl1101->reset_gpio_num >= 0) { + esp_rom_gpio_pad_select_gpio(jl1101->reset_gpio_num); + gpio_set_direction(jl1101->reset_gpio_num, GPIO_MODE_OUTPUT); + gpio_set_level(jl1101->reset_gpio_num, 0); + esp_rom_delay_us(100); // insert min input assert time + gpio_set_level(jl1101->reset_gpio_num, 1); + } + return ESP_OK; +} + +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* in case any link status has changed, let's assume we're in link down status */ + jl1101->link_status = ETH_LINK_DOWN; + /* Restart auto negotiation */ + bmcr_reg_t bmcr = { + .speed_select = 1, /* 100Mbps */ + .duplex_mode = 1, /* Full Duplex */ + .en_auto_nego = 1, /* Auto Negotiation */ + .restart_auto_nego = 1 /* Restart Auto Negotiation */ + }; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for auto negotiation complete */ + bmsr_reg_t bmsr; + uint32_t to = 0; + for (to = 0; to < jl1101->autonego_timeout_ms / 100; to++) { + vTaskDelay(pdMS_TO_TICKS(100)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + if (bmsr.auto_nego_complete) { + break; + } + } + /* Auto negotiation failed, maybe no network cable plugged in, so output a warning */ + if (to >= jl1101->autonego_timeout_ms / 100) { + ESP_LOGW(TAG, "auto negotiation timeout"); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_pwrctl(esp_eth_phy_t *phy, bool enable) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!enable) { + /* Enable IEEE Power Down Mode */ + bmcr.power_down = 1; + } else { + /* Disable IEEE Power Down Mode */ + bmcr.power_down = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + if (!enable) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + PHY_CHECK(bmcr.power_down == 1, "power down failed", err); + } else { + /* wait for power up complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 10; to++) { + vTaskDelay(pdMS_TO_TICKS(10)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.power_down == 0) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 10, "power up timeout", err); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_addr(esp_eth_phy_t *phy, uint32_t addr) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->addr = addr; + return ESP_OK; +} + +static esp_err_t jl1101_get_addr(esp_eth_phy_t *phy, uint32_t *addr) { + PHY_CHECK(addr, "addr can't be null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + *addr = jl1101->addr; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_del(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + free(jl1101); + return ESP_OK; +} + +static esp_err_t jl1101_advertise_pause_ability(esp_eth_phy_t *phy, uint32_t ability) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* Set PAUSE function ability */ + anar_reg_t anar; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, &(anar.val)) == ESP_OK, "read ANAR failed", + err); + if (ability) { + anar.asymmetric_pause = 1; + anar.symmetric_pause = 1; + } else { + anar.asymmetric_pause = 0; + anar.symmetric_pause = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, anar.val) == ESP_OK, "write ANAR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_init(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + // Detect PHY address + if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { + PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); + } + /* Power on Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); + /* Reset Ethernet PHY */ + PHY_CHECK(jl1101_reset(phy) == ESP_OK, "reset failed", err); + /* Check PHY ID */ + phyidr1_reg_t id1; + phyidr2_reg_t id2; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR1_REG_ADDR, &(id1.val)) == ESP_OK, "read ID1 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR2_REG_ADDR, &(id2.val)) == ESP_OK, "read ID2 failed", err); + PHY_CHECK(id1.oui_msb == 0x937C && id2.oui_lsb == 0x10 && id2.vendor_model == 0x2, "wrong chip ID", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_deinit(esp_eth_phy_t *phy) { + /* Power off Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, false) == ESP_OK, "power control failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { + PHY_CHECK(config, "can't set phy config to null", err); + phy_jl1101_t *jl1101 = calloc(1, sizeof(phy_jl1101_t)); + PHY_CHECK(jl1101, "calloc jl1101 failed", err); + jl1101->addr = config->phy_addr; + jl1101->reset_gpio_num = config->reset_gpio_num; + jl1101->reset_timeout_ms = config->reset_timeout_ms; + jl1101->link_status = ETH_LINK_DOWN; + jl1101->autonego_timeout_ms = config->autonego_timeout_ms; + jl1101->parent.reset = jl1101_reset; + jl1101->parent.reset_hw = jl1101_reset_hw; + jl1101->parent.init = jl1101_init; + jl1101->parent.deinit = jl1101_deinit; + jl1101->parent.set_mediator = jl1101_set_mediator; + jl1101->parent.negotiate = jl1101_negotiate; + jl1101->parent.get_link = jl1101_get_link; + jl1101->parent.pwrctl = jl1101_pwrctl; + jl1101->parent.get_addr = jl1101_get_addr; + jl1101->parent.set_addr = jl1101_set_addr; + jl1101->parent.advertise_pause_ability = jl1101_advertise_pause_ability; + jl1101->parent.del = jl1101_del; + + return &(jl1101->parent); +err: + return NULL; +} +#endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fa66d824be..a3f0ae715f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -71,6 +71,10 @@ void EthernetComponent::setup() { phy = esp_eth_phy_new_ip101(&phy_config); break; } + case ETHERNET_TYPE_JL1101: { + phy = esp_eth_phy_new_jl1101(&phy_config); + break; + } default: { this->mark_failed(); return; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index ed784e1bc0..a538a5c77d 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -18,6 +18,7 @@ enum EthernetType { ETHERNET_TYPE_RTL8201, ETHERNET_TYPE_DP83848, ETHERNET_TYPE_IP101, + ETHERNET_TYPE_JL1101, }; struct ManualIP { @@ -82,6 +83,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; +extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); } // namespace ethernet } // namespace esphome diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 4961595505..f95d679c3e 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -52,7 +52,7 @@ GrowattSolar = growatt_solar_ns.class_( PHASE_SENSORS = { CONF_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, ), CONF_CURRENT: sensor.sensor_schema( @@ -71,7 +71,7 @@ PHASE_SENSORS = { PV_SENSORS = { CONF_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, ), CONF_CURRENT: sensor.sensor_schema( @@ -135,13 +135,13 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=0, + accuracy_decimals=1, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), diff --git a/esphome/components/hte501/__init__.py b/esphome/components/hte501/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp new file mode 100644 index 0000000000..68edd07a22 --- /dev/null +++ b/esphome/components/hte501/hte501.cpp @@ -0,0 +1,90 @@ +#include "hte501.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hte501 { + +static const char *const TAG = "hte501"; + +void HTE501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up HTE501..."); + uint8_t address[] = {0x70, 0x29}; + this->write(address, 2, false); + uint8_t identification[9]; + this->read(identification, 9); + if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +} + +void HTE501Component::dump_config() { + ESP_LOGCONFIG(TAG, "HTE501:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with HTE501 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } +void HTE501Component::update() { + uint8_t address_1[] = {0x2C, 0x1B}; + this->write(address_1, 2, true); + this->set_timeout(50, [this]() { + uint8_t i2c_response[6]; + this->read(i2c_response, 6); + if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return; + } + float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]); + if (temperature > 55536) { + temperature = (temperature - 65536) / 100; + } else { + temperature = temperature / 100; + } + float humidity = ((float) encode_uint16(i2c_response[3], i2c_response[4])) / 100.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + }); +} + +unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { + unsigned char crc_val = 0xFF; + unsigned char i = 0; + unsigned char j = 0; + for (i = from; i <= to; i++) { + int cur_val = buf[i]; + for (j = 0; j < 8; j++) { + if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal + { + crc_val = ((crc_val << 1) ^ 0x31); + } else { + crc_val = (crc_val << 1); + } + cur_val = cur_val << 1; + } + } + return crc_val; +} +} // namespace hte501 +} // namespace esphome diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h new file mode 100644 index 0000000000..0d2c952e81 --- /dev/null +++ b/esphome/components/hte501/hte501.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hte501 { + +/// This class implements support for the hte501 of temperature i2c sensors. +class HTE501Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace hte501 +} // namespace esphome diff --git a/esphome/components/hte501/sensor.py b/esphome/components/hte501/sensor.py new file mode 100644 index 0000000000..8bd6160038 --- /dev/null +++ b/esphome/components/hte501/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +hte501_ns = cg.esphome_ns.namespace("hte501") +HTE501Component = hte501_ns.class_( + "HTE501Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HTE501Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index c8c0ca5369..0c3e249512 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -195,6 +195,8 @@ async def http_request_action_to_code(config, action_id, template_arg, args): for conf in config.get(CONF_ON_RESPONSE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) cg.add(var.register_response_trigger(trigger)) - await automation.build_automation(trigger, [(int, "status_code")], conf) + await automation.build_automation( + trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf + ) return var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 4e1cfe94b3..46894a9afd 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -66,6 +66,9 @@ void HttpRequestComponent::send(const std::vector } this->client_.setTimeout(this->timeout_); +#if defined(USE_ESP32) + this->client_.setConnectTimeout(this->timeout_); +#endif if (this->useragent_ != nullptr) { this->client_.setUserAgent(this->useragent_); } @@ -73,25 +76,27 @@ void HttpRequestComponent::send(const std::vector this->client_.addHeader(header.name, header.value, false, true); } + uint32_t start_time = millis(); int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); + uint32_t duration = millis() - start_time; for (auto *trigger : response_triggers) - trigger->process(http_code); + trigger->process(http_code, duration); if (http_code < 0) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_.c_str(), - HTTPClient::errorToString(http_code).c_str()); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(), + HTTPClient::errorToString(http_code).c_str(), duration); this->status_set_warning(); return; } if (http_code < 200 || http_code >= 300) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_.c_str(), http_code); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); this->status_set_warning(); return; } this->status_clear_warning(); - ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_.c_str(), http_code); + ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); } #ifdef USE_ESP8266 diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index eab3045fdc..0958c07683 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -31,7 +31,10 @@ struct Header { const char *value; }; -class HttpRequestResponseTrigger; +class HttpRequestResponseTrigger : public Trigger { + public: + void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); } +}; class HttpRequestComponent : public Component { public: @@ -138,11 +141,6 @@ template class HttpRequestSendAction : public Action { std::vector response_triggers_; }; -class HttpRequestResponseTrigger : public Trigger { - public: - void process(int status_code) { this->trigger(status_code); } -}; - } // namespace http_request } // namespace esphome diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 730499a493..c2dbbd6737 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -6,8 +6,10 @@ from esphome.const import ( CONF_MODEL, CONF_MOISTURE, CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRECIPITATION_INTENSITY, + DEVICE_CLASS_PRECIPITATION, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, ICON_THERMOMETER, ) @@ -70,31 +72,31 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ACC): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_PRECIPITATION, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_R_INT): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR, accuracy_decimals=2, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( unit_of_measurement=UNIT_INTENSITY, accuracy_decimals=0, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 3122b45bbe..0b87a0c4eb 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import core, pins from esphome.components import display, spi from esphome.const import ( CONF_COLOR_PALETTE, @@ -12,10 +12,11 @@ from esphome.const import ( CONF_RAW_DATA_ID, CONF_RESET_PIN, ) -from esphome.core import HexInt +from esphome.core import CORE, HexInt DEPENDENCIES = ["spi"] +CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_LED_PIN = "led_pin" ili9341_ns = cg.esphome_ns.namespace("ili9341") @@ -37,7 +38,25 @@ MODELS = { ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_") -COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE") +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") + + +def _validate(config): + if config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and not config.get( + CONF_COLOR_PALETTE_IMAGES + ): + raise cv.Invalid( + "Color palette in IMAGE_ADAPTIVE mode requires at least one 'color_palette_images' entry to generate palette" + ) + if ( + config.get(CONF_COLOR_PALETTE_IMAGES) + and config.get(CONF_COLOR_PALETTE) != "IMAGE_ADAPTIVE" + ): + raise cv.Invalid( + "Providing color palette images requires palette mode to be 'IMAGE_ADAPTIVE'" + ) + return config + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( @@ -48,12 +67,16 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE, + cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( + cv.file_ + ), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), } ) .extend(cv.polling_component_schema("1s")) .extend(spi.spi_device_schema(False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate, ) @@ -86,12 +109,45 @@ async def to_code(config): led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN]) cg.add(var.set_led_pin(led_pin)) + rhs = None if config[CONF_COLOR_PALETTE] == "GRAYSCALE": cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED)) rhs = [] for x in range(256): rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) + elif config[CONF_COLOR_PALETTE] == "IMAGE_ADAPTIVE": + cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED)) + from PIL import Image + + def load_image(filename): + path = CORE.relative_config_path(filename) + try: + return Image.open(path) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") + + # make a wide horizontal combined image. + images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]] + total_width = sum(i.width for i in images) + max_height = max(i.height for i in images) + + ref_image = Image.new("RGB", (total_width, max_height)) + x = 0 + for i in images: + ref_image.paste(i, (x, 0)) + x = x + i.width + + # reduce the colors on combined image to 256. + converted = ref_image.convert("P", palette=Image.ADAPTIVE, colors=256) + # if you want to verify how the images look use + # ref_image.save("ref_in.png") + # converted.save("ref_out.png") + palette = converted.getpalette() + assert len(palette) == 256 * 3 + rhs = palette + else: + cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8)) + + if rhs is not None: prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) - else: - pass diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index 117de3de89..9f9edcf21f 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -122,7 +122,12 @@ void ILI9341Display::display_() { } void ILI9341Display::fill(Color color) { - uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + uint8_t color332 = 0; + if (this->buffer_color_mode_ == BITS_8) { + color332 = display::ColorUtil::color_to_332(color); + } else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) + color332 = display::ColorUtil::color_to_index8_palette888(color, this->palette_); + } memset(this->buffer_, color332, this->get_buffer_length_()); this->x_low_ = 0; this->y_low_ = 0; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 0004391f20..88c625961b 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -26,6 +26,7 @@ IMAGE_TYPE = { "RGB24": ImageType.IMAGE_TYPE_RGB24, "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, "RGB565": ImageType.IMAGE_TYPE_RGB565, + "TRANSPARENT_IMAGE": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, } Image_ = display.display_ns.class_("Image") @@ -105,7 +106,7 @@ async def to_code(config): data[pos] = rgb & 255 pos += 1 - elif config[CONF_TYPE] == "BINARY": + elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"): image = image.convert("1", dither=dither) width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] @@ -116,7 +117,7 @@ async def to_code(config): pos = x + y * width8 data[pos // 8] |= 0x80 >> (pos % 8) - elif config[CONF_TYPE] == "TRANSPARENT_BINARY": + elif config[CONF_TYPE] == "TRANSPARENT_IMAGE": image = image.convert("RGBA") width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py new file mode 100644 index 0000000000..5c2853a5c6 --- /dev/null +++ b/esphome/components/improv_base/__init__.py @@ -0,0 +1,42 @@ +import re + +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import __version__ + +CODEOWNERS = ["@esphome/core"] + +CONF_NEXT_URL = "next_url" + +VALID_SUBSTITUTIONS = ["esphome_version", "ip_address", "device_name"] + + +def validate_next_url(value): + value = cv.url(value) + test = r"{{(?!" + r"\b|".join(VALID_SUBSTITUTIONS) + r"\b)(\w+)}}" + result = re.search(test, value) + if result: + raise cv.Invalid( + f"Invalid substitution(s) ({', '.join(result.groups())}) in next_url. Valid substitutions are: {', '.join(VALID_SUBSTITUTIONS)}" + ) + return value + + +IMPROV_SCHEMA = cv.Schema( + { + cv.Optional(CONF_NEXT_URL): validate_next_url, + } +) + + +def _process_next_url(url: str): + if "{{esphome_version}}" in url: + url = url.replace("{{esphome_version}}", __version__) + return url + + +async def setup_improv_core(var, config): + if CONF_NEXT_URL in config: + cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) + cg.add_library("esphome/Improv", "1.2.3") diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp new file mode 100644 index 0000000000..c95b3a77ec --- /dev/null +++ b/esphome/components/improv_base/improv_base.cpp @@ -0,0 +1,32 @@ +#include "improv_base.h" + +#include "esphome/components/network/util.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace improv_base { + +std::string ImprovBase::get_formatted_next_url_() { + if (this->next_url_.empty()) { + return ""; + } + std::string copy = this->next_url_; + // Device name + std::size_t pos = this->next_url_.find("{{device_name}}"); + if (pos != std::string::npos) { + const std::string &device_name = App.get_name(); + copy.replace(pos, 15, device_name); + } + + // Ip address + pos = this->next_url_.find("{{ip_address}}"); + if (pos != std::string::npos) { + std::string ip = network::IPAddress(network::get_ip_address()).str(); + copy.replace(pos, 14, ip); + } + + return copy; +} + +} // namespace improv_base +} // namespace esphome diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h new file mode 100644 index 0000000000..90cd02a4ab --- /dev/null +++ b/esphome/components/improv_base/improv_base.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace esphome { +namespace improv_base { + +class ImprovBase { + public: + void set_next_url(const std::string &next_url) { this->next_url_ = next_url; } + + protected: + std::string get_formatted_next_url_(); + std::string next_url_; +}; + +} // namespace improv_base +} // namespace esphome diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 4a6da6bee0..311256804b 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -4,7 +4,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE import esphome.final_validate as fv +from esphome.components import improv_base +AUTO_LOAD = ["improv_base"] CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["logger", "wifi"] @@ -12,11 +14,15 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial") ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ImprovSerialComponent), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ImprovSerialComponent), + } + ) + .extend(improv_base.IMPROV_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) def validate_logger(config): @@ -37,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add_library("esphome/Improv", "1.2.3") + await improv_base.setup_improv_core(var, config) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 0dab71060c..fe19e2f085 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -95,6 +95,9 @@ void ImprovSerialComponent::loop() { std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { std::vector urls; + if (!this->next_url_.empty()) { + urls.push_back(this->get_formatted_next_url_()); + } #ifdef USE_WEBSERVER auto ip = wifi::global_wifi_component->wifi_sta_ip(); std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 3d8478252d..731f9f9984 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/components/improv_base/improv_base.h" #include "esphome/components/wifi/wifi_component.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -27,7 +28,7 @@ enum ImprovSerialType : uint8_t { static const uint8_t IMPROV_SERIAL_VERSION = 1; -class ImprovSerialComponent : public Component { +class ImprovSerialComponent : public Component, public improv_base::ImprovBase { public: void setup() override; void loop() override; diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 7e701af48b..f105280b23 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -7,6 +7,9 @@ #ifdef USE_ESP32 #include #endif +#ifdef USE_RP2040 +#include +#endif namespace esphome { namespace json { @@ -24,6 +27,8 @@ std::string build_json(const json_build_t &f) { const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); +#elif defined(USE_RP2040) + const size_t free_heap = rp2040.getFreeHeap(); #endif size_t request_size = std::min(free_heap, (size_t) 512); @@ -64,6 +69,8 @@ void parse_json(const std::string &data, const json_parse_t &f) { const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); +#elif defined(USE_RP2040) + const size_t free_heap = rp2040.getFreeHeap(); #endif bool pass = false; size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5)); diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py new file mode 100644 index 0000000000..2099e28109 --- /dev/null +++ b/esphome/components/key_collector/__init__.py @@ -0,0 +1,95 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import key_provider +from esphome.const import ( + CONF_ID, + CONF_MAX_LENGTH, + CONF_MIN_LENGTH, + CONF_ON_TIMEOUT, + CONF_SOURCE_ID, + CONF_TIMEOUT, +) + +CODEOWNERS = ["@ssieb"] + +MULTI_CONF = True + +AUTO_LOAD = ["key_provider"] + +CONF_START_KEYS = "start_keys" +CONF_END_KEYS = "end_keys" +CONF_END_KEY_REQUIRED = "end_key_required" +CONF_BACK_KEYS = "back_keys" +CONF_CLEAR_KEYS = "clear_keys" +CONF_ALLOWED_KEYS = "allowed_keys" +CONF_ON_PROGRESS = "on_progress" +CONF_ON_RESULT = "on_result" + +key_collector_ns = cg.esphome_ns.namespace("key_collector") +KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(KeyCollector), + cv.GenerateID(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), + cv.Optional(CONF_MIN_LENGTH): cv.int_, + cv.Optional(CONF_MAX_LENGTH): cv.int_, + cv.Optional(CONF_START_KEYS): cv.string, + cv.Optional(CONF_END_KEYS): cv.string, + cv.Optional(CONF_END_KEY_REQUIRED): cv.boolean, + cv.Optional(CONF_BACK_KEYS): cv.string, + cv.Optional(CONF_CLEAR_KEYS): cv.string, + cv.Optional(CONF_ALLOWED_KEYS): cv.string, + cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), + cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds, + } + ), + cv.has_at_least_one_key(CONF_END_KEYS, CONF_MAX_LENGTH), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_provider(source)) + if CONF_MIN_LENGTH in config: + cg.add(var.set_min_length(config[CONF_MIN_LENGTH])) + if CONF_MAX_LENGTH in config: + cg.add(var.set_max_length(config[CONF_MAX_LENGTH])) + if CONF_START_KEYS in config: + cg.add(var.set_start_keys(config[CONF_START_KEYS])) + if CONF_END_KEYS in config: + cg.add(var.set_end_keys(config[CONF_END_KEYS])) + if CONF_END_KEY_REQUIRED in config: + cg.add(var.set_end_key_required(config[CONF_END_KEY_REQUIRED])) + if CONF_BACK_KEYS in config: + cg.add(var.set_back_keys(config[CONF_BACK_KEYS])) + if CONF_CLEAR_KEYS in config: + cg.add(var.set_clear_keys(config[CONF_CLEAR_KEYS])) + if CONF_ALLOWED_KEYS in config: + cg.add(var.set_allowed_keys(config[CONF_ALLOWED_KEYS])) + if CONF_ON_PROGRESS in config: + await automation.build_automation( + var.get_progress_trigger(), + [(cg.std_string, "x"), (cg.uint8, "start")], + config[CONF_ON_PROGRESS], + ) + if CONF_ON_RESULT in config: + await automation.build_automation( + var.get_result_trigger(), + [(cg.std_string, "x"), (cg.uint8, "start"), (cg.uint8, "end")], + config[CONF_ON_RESULT], + ) + if CONF_ON_TIMEOUT in config: + await automation.build_automation( + var.get_timeout_trigger(), + [(cg.std_string, "x"), (cg.uint8, "start")], + config[CONF_ON_TIMEOUT], + ) + if CONF_TIMEOUT in config: + cg.add(var.set_timeout(config[CONF_TIMEOUT])) diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp new file mode 100644 index 0000000000..a9213890ee --- /dev/null +++ b/esphome/components/key_collector/key_collector.cpp @@ -0,0 +1,95 @@ +#include "key_collector.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace key_collector { + +static const char *const TAG = "key_collector"; + +KeyCollector::KeyCollector() + : progress_trigger_(new Trigger()), + result_trigger_(new Trigger()), + timeout_trigger_(new Trigger()) {} + +void KeyCollector::loop() { + if ((this->timeout_ == 0) || this->result_.empty() || (millis() - this->last_key_time_ < this->timeout_)) + return; + this->timeout_trigger_->trigger(this->result_, this->start_key_); + this->clear(); +} + +void KeyCollector::dump_config() { + ESP_LOGCONFIG(TAG, "Key Collector:"); + if (this->min_length_ > 0) + ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_); + if (this->max_length_ > 0) + ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_); + if (!this->back_keys_.empty()) + ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str()); + if (!this->clear_keys_.empty()) + ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str()); + if (!this->start_keys_.empty()) + ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str()); + if (!this->end_keys_.empty()) { + ESP_LOGCONFIG(TAG, " end keys '%s'", this->end_keys_.c_str()); + ESP_LOGCONFIG(TAG, " end key is required: %s", ONOFF(this->end_key_required_)); + } + if (!this->allowed_keys_.empty()) + ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str()); + if (this->timeout_ > 0) + ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0); +} + +void KeyCollector::set_provider(key_provider::KeyProvider *provider) { + provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); }); +} + +void KeyCollector::clear(bool progress_update) { + this->result_.clear(); + this->start_key_ = 0; + if (progress_update) + this->progress_trigger_->trigger(this->result_, 0); +} + +void KeyCollector::key_pressed_(uint8_t key) { + this->last_key_time_ = millis(); + if (!this->start_keys_.empty() && !this->start_key_) { + if (this->start_keys_.find(key) != std::string::npos) { + this->start_key_ = key; + this->progress_trigger_->trigger(this->result_, this->start_key_); + } + return; + } + if (this->back_keys_.find(key) != std::string::npos) { + if (!this->result_.empty()) { + this->result_.pop_back(); + this->progress_trigger_->trigger(this->result_, this->start_key_); + } + return; + } + if (this->clear_keys_.find(key) != std::string::npos) { + if (!this->result_.empty()) + this->clear(); + return; + } + if (this->end_keys_.find(key) != std::string::npos) { + if ((this->min_length_ == 0) || (this->result_.size() >= this->min_length_)) { + this->result_trigger_->trigger(this->result_, this->start_key_, key); + this->clear(); + } + return; + } + if (!this->allowed_keys_.empty() && (this->allowed_keys_.find(key) == std::string::npos)) + return; + if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) + this->result_.push_back(key); + if ((this->max_length_ > 0) && (this->result_.size() == this->max_length_) && (!this->end_key_required_)) { + this->result_trigger_->trigger(this->result_, this->start_key_, 0); + this->clear(false); + } + this->progress_trigger_->trigger(this->result_, this->start_key_); +} + +} // namespace key_collector +} // namespace esphome diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h new file mode 100644 index 0000000000..5e63397839 --- /dev/null +++ b/esphome/components/key_collector/key_collector.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace key_collector { + +class KeyCollector : public Component { + public: + KeyCollector(); + void loop() override; + void dump_config() override; + void set_provider(key_provider::KeyProvider *provider); + void set_min_length(int min_length) { this->min_length_ = min_length; }; + void set_max_length(int max_length) { this->max_length_ = max_length; }; + void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; + void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; + void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; + void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); }; + void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); }; + void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); }; + Trigger *get_progress_trigger() const { return this->progress_trigger_; }; + Trigger *get_result_trigger() const { return this->result_trigger_; }; + Trigger *get_timeout_trigger() const { return this->timeout_trigger_; }; + void set_timeout(int timeout) { this->timeout_ = timeout; }; + + void clear(bool progress_update = true); + + protected: + void key_pressed_(uint8_t key); + + int min_length_{0}; + int max_length_{0}; + std::string start_keys_; + std::string end_keys_; + bool end_key_required_{false}; + std::string back_keys_; + std::string clear_keys_; + std::string allowed_keys_; + std::string result_; + uint8_t start_key_{0}; + Trigger *progress_trigger_; + Trigger *result_trigger_; + Trigger *timeout_trigger_; + uint32_t last_key_time_; + uint32_t timeout_{0}; +}; + +} // namespace key_collector +} // namespace esphome diff --git a/esphome/components/key_provider/__init__.py b/esphome/components/key_provider/__init__.py new file mode 100644 index 0000000000..f397382ff2 --- /dev/null +++ b/esphome/components/key_provider/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@ssieb"] + +key_provider_ns = cg.esphome_ns.namespace("key_provider") +KeyProvider = key_provider_ns.class_("KeyProvider") diff --git a/esphome/components/key_provider/key_provider.cpp b/esphome/components/key_provider/key_provider.cpp new file mode 100644 index 0000000000..5a0e24b13f --- /dev/null +++ b/esphome/components/key_provider/key_provider.cpp @@ -0,0 +1,13 @@ +#include "key_provider.h" + +namespace esphome { +namespace key_provider { + +void KeyProvider::add_on_key_callback(std::function &&callback) { + this->key_callback_.add(std::move(callback)); +} + +void KeyProvider::send_key_(uint8_t key) { this->key_callback_.call(key); } + +} // namespace key_provider +} // namespace esphome diff --git a/esphome/components/key_provider/key_provider.h b/esphome/components/key_provider/key_provider.h new file mode 100644 index 0000000000..272d3eecad --- /dev/null +++ b/esphome/components/key_provider/key_provider.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace key_provider { + +/// interface for components that provide keypresses +class KeyProvider { + public: + void add_on_key_callback(std::function &&callback); + + protected: + void send_key_(uint8_t key); + + CallbackManager key_callback_{}; +}; + +} // namespace key_provider +} // namespace esphome diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py new file mode 100644 index 0000000000..be39cc2979 --- /dev/null +++ b/esphome/components/ld2410/__init__.py @@ -0,0 +1,158 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome import automation +from esphome.automation import maybe_simple_id + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@sebcaps"] +MULTI_CONF = True + +ld2410_ns = cg.esphome_ns.namespace("ld2410") +LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice) +LD2410Restart = ld2410_ns.class_("LD2410Restart", automation.Action) +CONF_LD2410_ID = "ld2410_id" +CONF_MAX_MOVE_DISTANCE = "max_move_distance" +CONF_MAX_STILL_DISTANCE = "max_still_distance" +CONF_G0_MOVE_THRESHOLD = "g0_move_threshold" +CONF_G0_STILL_THRESHOLD = "g0_still_threshold" +CONF_G1_MOVE_THRESHOLD = "g1_move_threshold" +CONF_G1_STILL_THRESHOLD = "g1_still_threshold" +CONF_G2_MOVE_THRESHOLD = "g2_move_threshold" +CONF_G2_STILL_THRESHOLD = "g2_still_threshold" +CONF_G3_MOVE_THRESHOLD = "g3_move_threshold" +CONF_G3_STILL_THRESHOLD = "g3_still_threshold" +CONF_G4_MOVE_THRESHOLD = "g4_move_threshold" +CONF_G4_STILL_THRESHOLD = "g4_still_threshold" +CONF_G5_MOVE_THRESHOLD = "g5_move_threshold" +CONF_G5_STILL_THRESHOLD = "g5_still_threshold" +CONF_G6_MOVE_THRESHOLD = "g6_move_threshold" +CONF_G6_STILL_THRESHOLD = "g6_still_threshold" +CONF_G7_MOVE_THRESHOLD = "g7_move_threshold" +CONF_G7_STILL_THRESHOLD = "g7_still_threshold" +CONF_G8_MOVE_THRESHOLD = "g8_move_threshold" +CONF_G8_STILL_THRESHOLD = "g8_still_threshold" + +DISTANCES = [0.75, 1.5, 2.25, 3, 3.75, 4.5, 5.25, 6] + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2410Component), + cv.Optional(CONF_MAX_MOVE_DISTANCE, default="4.5m"): cv.All( + cv.distance, cv.one_of(*DISTANCES, float=True) + ), + cv.Optional(CONF_MAX_STILL_DISTANCE, default="4.5m"): cv.All( + cv.distance, cv.one_of(*DISTANCES, float=True) + ), + cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( + cv.positive_time_period_seconds, + cv.Range(max=cv.TimePeriod(seconds=32767)), + ), + cv.Optional(CONF_G0_MOVE_THRESHOLD, default=50): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G0_STILL_THRESHOLD, default=0): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G1_MOVE_THRESHOLD, default=50): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G1_STILL_THRESHOLD, default=0): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G2_MOVE_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G2_STILL_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G3_MOVE_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G3_STILL_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G4_MOVE_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G4_STILL_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G5_MOVE_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G5_STILL_THRESHOLD, default=40): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G6_MOVE_THRESHOLD, default=30): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G6_STILL_THRESHOLD, default=15): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G7_MOVE_THRESHOLD, default=30): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G7_STILL_THRESHOLD, default=15): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G8_MOVE_THRESHOLD, default=30): cv.int_range( + min=0, max=100 + ), + cv.Optional(CONF_G8_STILL_THRESHOLD, default=15): cv.int_range( + min=0, max=100 + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2410", + baud_rate=256000, + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(var.set_timeout(config[CONF_TIMEOUT])) + cg.add(var.set_max_move_distance(int(config[CONF_MAX_MOVE_DISTANCE] / 0.75))) + cg.add(var.set_max_still_distance(int(config[CONF_MAX_STILL_DISTANCE] / 0.75))) + cg.add( + var.set_range_config( + config[CONF_G0_MOVE_THRESHOLD], + config[CONF_G0_STILL_THRESHOLD], + config[CONF_G1_MOVE_THRESHOLD], + config[CONF_G1_STILL_THRESHOLD], + config[CONF_G2_MOVE_THRESHOLD], + config[CONF_G2_STILL_THRESHOLD], + config[CONF_G3_MOVE_THRESHOLD], + config[CONF_G3_STILL_THRESHOLD], + config[CONF_G4_MOVE_THRESHOLD], + config[CONF_G4_STILL_THRESHOLD], + config[CONF_G5_MOVE_THRESHOLD], + config[CONF_G5_STILL_THRESHOLD], + config[CONF_G6_MOVE_THRESHOLD], + config[CONF_G6_STILL_THRESHOLD], + config[CONF_G7_MOVE_THRESHOLD], + config[CONF_G7_STILL_THRESHOLD], + config[CONF_G8_MOVE_THRESHOLD], + config[CONF_G8_STILL_THRESHOLD], + ) + ) + + +CALIBRATION_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(LD2410Component), + } +) diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py new file mode 100644 index 0000000000..02f73d57b7 --- /dev/null +++ b/esphome/components/ld2410/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY +from . import CONF_LD2410_ID, LD2410Component + +DEPENDENCIES = ["ld2410"] +CONF_HAS_TARGET = "has_target" +CONF_HAS_MOVING_TARGET = "has_moving_target" +CONF_HAS_STILL_TARGET = "has_still_target" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY + ), + cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION + ), + cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if CONF_HAS_TARGET in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET]) + cg.add(ld2410_component.set_target_sensor(sens)) + if CONF_HAS_MOVING_TARGET in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_MOVING_TARGET]) + cg.add(ld2410_component.set_moving_target_sensor(sens)) + if CONF_HAS_STILL_TARGET in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_STILL_TARGET]) + cg.add(ld2410_component.set_still_target_sensor(sens)) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp new file mode 100644 index 0000000000..8e67ba54d7 --- /dev/null +++ b/esphome/components/ld2410/ld2410.cpp @@ -0,0 +1,315 @@ +#include "ld2410.h" + +#define highbyte(val) (uint8_t)((val) >> 8) +#define lowbyte(val) (uint8_t)((val) &0xff) + +namespace esphome { +namespace ld2410 { + +static const char *const TAG = "ld2410"; + +void LD2410Component::dump_config() { + ESP_LOGCONFIG(TAG, "LD2410:"); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "HasTargetSensor", this->target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingSensor", this->moving_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillSensor", this->still_binary_sensor_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Moving Distance", this->moving_target_distance_sensor_); + LOG_SENSOR(" ", "Still Distance", this->still_target_distance_sensor_); + LOG_SENSOR(" ", "Moving Energy", this->moving_target_energy_sensor_); + LOG_SENSOR(" ", "Still Energy", this->still_target_energy_sensor_); + LOG_SENSOR(" ", "Detection Distance", this->detection_distance_sensor_); +#endif + this->set_config_mode_(true); + this->get_version_(); + this->set_config_mode_(false); + ESP_LOGCONFIG(TAG, " Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2], + this->version_[3], this->version_[4], this->version_[5]); +} + +void LD2410Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up LD2410..."); + this->set_config_mode_(true); + this->set_max_distances_timeout_(this->max_move_distance_, this->max_still_distance_, this->timeout_); + // Configure Gates sensitivity + this->set_gate_threshold_(0, this->rg0_move_threshold_, this->rg0_still_threshold_); + this->set_gate_threshold_(1, this->rg1_move_threshold_, this->rg1_still_threshold_); + this->set_gate_threshold_(2, this->rg2_move_threshold_, this->rg2_still_threshold_); + this->set_gate_threshold_(3, this->rg3_move_threshold_, this->rg3_still_threshold_); + this->set_gate_threshold_(4, this->rg4_move_threshold_, this->rg4_still_threshold_); + this->set_gate_threshold_(5, this->rg5_move_threshold_, this->rg5_still_threshold_); + this->set_gate_threshold_(6, this->rg6_move_threshold_, this->rg6_still_threshold_); + this->set_gate_threshold_(7, this->rg7_move_threshold_, this->rg7_still_threshold_); + this->set_gate_threshold_(8, this->rg8_move_threshold_, this->rg8_still_threshold_); + this->get_version_(); + this->set_config_mode_(false); + ESP_LOGCONFIG(TAG, "Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2], + this->version_[3], this->version_[4], this->version_[5]); + ESP_LOGCONFIG(TAG, "LD2410 setup complete."); +} + +void LD2410Component::loop() { + const int max_line_length = 80; + static uint8_t buffer[max_line_length]; + + while (available()) { + this->readline_(read(), buffer, max_line_length); + } +} + +void LD2410Component::send_command_(uint8_t command, uint8_t *command_value, int command_value_len) { + // lastCommandSuccess->publish_state(false); + + // frame start bytes + this->write_array(CMD_FRAME_HEADER, 4); + // length bytes + int len = 2; + if (command_value != nullptr) + len += command_value_len; + this->write_byte(lowbyte(len)); + this->write_byte(highbyte(len)); + + // command + this->write_byte(lowbyte(command)); + this->write_byte(highbyte(command)); + + // command value bytes + if (command_value != nullptr) { + for (int i = 0; i < command_value_len; i++) { + this->write_byte(command_value[i]); + } + } + // frame end bytes + this->write_array(CMD_FRAME_END, 4); + // FIXME to remove + delay(50); // NOLINT +} + +void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { + if (len < 12) + return; // 4 frame start bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame end bytes + if (buffer[0] != 0xF4 || buffer[1] != 0xF3 || buffer[2] != 0xF2 || buffer[3] != 0xF1) // check 4 frame start bytes + return; + if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK) // Check constant values + return; // data head=0xAA, data end=0x55, crc=0x00 + + /* + Data Type: 6th + 0x01: Engineering mode + 0x02: Normal mode + */ + // char data_type = buffer[DATA_TYPES]; + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ +#ifdef USE_BINARY_SENSOR + char target_state = buffer[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } +#endif + + /* + Reduce data update rate to prevent home assistant database size grow fast + */ + int32_t current_millis = millis(); + if (current_millis - last_periodic_millis < 1000) + return; + last_periodic_millis = current_millis; + +#ifdef USE_BINARY_SENSOR + if (this->moving_binary_sensor_ != nullptr) { + this->moving_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + } + if (this->still_binary_sensor_ != nullptr) { + this->still_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); + } +#endif + /* + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes + */ +#ifdef USE_SENSOR + if (this->moving_target_distance_sensor_ != nullptr) { + int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); + if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) + this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); + } + if (this->moving_target_energy_sensor_ != nullptr) { + int new_moving_target_energy = buffer[MOVING_ENERGY]; + if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy) + this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); + } + if (this->still_target_distance_sensor_ != nullptr) { + int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); + if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) + this->still_target_distance_sensor_->publish_state(new_still_target_distance); + } + if (this->still_target_energy_sensor_ != nullptr) { + int new_still_target_energy = buffer[STILL_ENERGY]; + if (this->still_target_energy_sensor_->get_state() != new_still_target_energy) + this->still_target_energy_sensor_->publish_state(new_still_target_energy); + } + if (this->detection_distance_sensor_ != nullptr) { + int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); + if (this->detection_distance_sensor_->get_state() != new_detect_distance) + this->detection_distance_sensor_->publish_state(new_detect_distance); + } +#endif +} + +void LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND"); + if (len < 10) { + ESP_LOGE(TAG, "Error with last command : incorrect length"); + return; + } + if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes + ESP_LOGE(TAG, "Error with last command : incorrect Header"); + return; + } + if (buffer[COMMAND_STATUS] != 0x01) { + ESP_LOGE(TAG, "Error with last command : status != 0x01"); + return; + } + if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { + ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); + return; + } + + switch (buffer[COMMAND]) { + case lowbyte(CMD_ENABLE_CONF): + ESP_LOGV(TAG, "Handled Enable conf command"); + break; + case lowbyte(CMD_DISABLE_CONF): + ESP_LOGV(TAG, "Handled Disabled conf command"); + break; + case lowbyte(CMD_VERSION): + ESP_LOGV(TAG, "FW Version is: %u.%u.%u%u%u%u", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], + buffer[14]); + this->version_[0] = buffer[13]; + this->version_[1] = buffer[12]; + this->version_[2] = buffer[17]; + this->version_[3] = buffer[16]; + this->version_[4] = buffer[15]; + this->version_[5] = buffer[14]; + + break; + case lowbyte(CMD_GATE_SENS): + ESP_LOGV(TAG, "Handled sensitivity command"); + break; + case lowbyte(CMD_QUERY): // Query parameters response + { + if (buffer[10] != 0xAA) + return; // value head=0xAA + /* + Moving distance range: 13th byte + Still distance range: 14th byte + */ + // TODO + // maxMovingDistanceRange->publish_state(buffer[12]); + // maxStillDistanceRange->publish_state(buffer[13]); + /* + Moving Sensitivities: 15~23th bytes + Still Sensitivities: 24~32th bytes + */ + for (int i = 0; i < 9; i++) { + moving_sensitivities[i] = buffer[14 + i]; + } + for (int i = 0; i < 9; i++) { + still_sensitivities[i] = buffer[23 + i]; + } + /* + None Duration: 33~34th bytes + */ + // noneDuration->publish_state(this->two_byte_to_int_(buffer[32], buffer[33])); + } break; + default: + break; + } +} + +void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { + static int pos = 0; + + if (readch >= 0) { + if (pos < len - 1) { + buffer[pos++] = readch; + buffer[pos] = 0; + } else { + pos = 0; + } + if (pos >= 4) { + if (buffer[pos - 4] == 0xF8 && buffer[pos - 3] == 0xF7 && buffer[pos - 2] == 0xF6 && buffer[pos - 1] == 0xF5) { + ESP_LOGV(TAG, "Will handle Periodic Data"); + this->handle_periodic_data_(buffer, pos); + pos = 0; // Reset position index ready for next time + } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 && + buffer[pos - 1] == 0x01) { + ESP_LOGV(TAG, "Will handle ACK Data"); + this->handle_ack_data_(buffer, pos); + pos = 0; // Reset position index ready for next time + } + } + } +} + +void LD2410Component::set_config_mode_(bool enable) { + uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, 2); +} + +void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); } +void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } + +void LD2410Component::set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range, + uint16_t timeout) { + uint8_t value[18] = {0x00, + 0x00, + lowbyte(max_moving_distance_range), + highbyte(max_moving_distance_range), + 0x00, + 0x00, + 0x01, + 0x00, + lowbyte(max_still_distance_range), + highbyte(max_still_distance_range), + 0x00, + 0x00, + 0x02, + 0x00, + lowbyte(timeout), + highbyte(timeout), + 0x00, + 0x00}; + this->send_command_(CMD_MAXDIST_DURATION, value, 18); + this->query_parameters_(); +} +void LD2410Component::set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens) { + // reference + // https://drive.google.com/drive/folders/1p4dhbEJA3YubyIjIIC7wwVsSo8x29Fq-?spm=a2g0o.detail.1000023.17.93465697yFwVxH + // Send data: configure the motion sensitivity of distance gate 3 to 40, and the static sensitivity of 40 + // 00 00 (gate) + // 03 00 00 00 (gate number) + // 01 00 (motion sensitivity) + // 28 00 00 00 (value) + // 02 00 (still sensitivtiy) + // 28 00 00 00 (value) + uint8_t value[18] = {0x00, 0x00, lowbyte(gate), highbyte(gate), 0x00, 0x00, + 0x01, 0x00, lowbyte(motionsens), highbyte(motionsens), 0x00, 0x00, + 0x02, 0x00, lowbyte(stillsens), highbyte(stillsens), 0x00, 0x00}; + this->send_command_(CMD_GATE_SENS, value, 18); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h new file mode 100644 index 0000000000..5a35798bc2 --- /dev/null +++ b/esphome/components/ld2410/ld2410.h @@ -0,0 +1,146 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ld2410 { + +#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) + +// Commands +static const uint8_t CMD_ENABLE_CONF = 0x00FF; +static const uint8_t CMD_DISABLE_CONF = 0x00FE; +static const uint8_t CMD_MAXDIST_DURATION = 0x0060; +static const uint8_t CMD_QUERY = 0x0061; +static const uint8_t CMD_GATE_SENS = 0x0064; +static const uint8_t CMD_VERSION = 0x00A0; + +// Commands values +static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; +static const uint8_t CMD_MAX_STILL_VALUE = 0x0001; +static const uint8_t CMD_DURATION_VALUE = 0x0002; +// Command Header & Footer +static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; +static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; +static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; +/* +Data Type: 6th byte +Target states: 9th byte + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes +*/ +enum PeriodicDataStructure : uint8_t { + DATA_TYPES = 5, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + DETECT_DISTANCE_LOW = 15, + DETECT_DISTANCE_HIGH = 16, +}; +enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; + +enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; + +// char cmd[2] = {enable ? 0xFF : 0xFE, 0x00}; +class LD2410Component : public Component, public uart::UARTDevice { +#ifdef USE_SENSOR + SUB_SENSOR(moving_target_distance) + SUB_SENSOR(still_target_distance) + SUB_SENSOR(moving_target_energy) + SUB_SENSOR(still_target_energy) + SUB_SENSOR(detection_distance) +#endif + + public: + void setup() override; + void dump_config() override; + void loop() override; + +#ifdef USE_BINARY_SENSOR + void set_target_sensor(binary_sensor::BinarySensor *sens) { this->target_binary_sensor_ = sens; }; + void set_moving_target_sensor(binary_sensor::BinarySensor *sens) { this->moving_binary_sensor_ = sens; }; + void set_still_target_sensor(binary_sensor::BinarySensor *sens) { this->still_binary_sensor_ = sens; }; +#endif + + void set_timeout(uint16_t value) { this->timeout_ = value; }; + void set_max_move_distance(uint8_t value) { this->max_move_distance_ = value; }; + void set_max_still_distance(uint8_t value) { this->max_still_distance_ = value; }; + void set_range_config(int rg0_move, int rg0_still, int rg1_move, int rg1_still, int rg2_move, int rg2_still, + int rg3_move, int rg3_still, int rg4_move, int rg4_still, int rg5_move, int rg5_still, + int rg6_move, int rg6_still, int rg7_move, int rg7_still, int rg8_move, int rg8_still) { + this->rg0_move_threshold_ = rg0_move; + this->rg0_still_threshold_ = rg0_still; + this->rg1_move_threshold_ = rg1_move; + this->rg1_still_threshold_ = rg1_still; + this->rg2_move_threshold_ = rg2_move; + this->rg2_still_threshold_ = rg2_still; + this->rg3_move_threshold_ = rg3_move; + this->rg3_still_threshold_ = rg3_still; + this->rg4_move_threshold_ = rg4_move; + this->rg4_still_threshold_ = rg4_still; + this->rg5_move_threshold_ = rg5_move; + this->rg5_still_threshold_ = rg5_still; + this->rg6_move_threshold_ = rg6_move; + this->rg6_still_threshold_ = rg6_still; + this->rg7_move_threshold_ = rg7_move; + this->rg7_still_threshold_ = rg7_still; + this->rg8_move_threshold_ = rg8_move; + this->rg8_still_threshold_ = rg8_still; + }; + int moving_sensitivities[9] = {0}; + int still_sensitivities[9] = {0}; + + int32_t last_periodic_millis = millis(); + + protected: +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *target_binary_sensor_{nullptr}; + binary_sensor::BinarySensor *moving_binary_sensor_{nullptr}; + binary_sensor::BinarySensor *still_binary_sensor_{nullptr}; +#endif + + std::vector rx_buffer_; + int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t)(secondbyte << 8) + firstbyte; } + void send_command_(uint8_t command_str, uint8_t *command_value, int command_value_len); + + void set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range, + uint16_t timeout); + void set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens); + void set_config_mode_(bool enable); + void handle_periodic_data_(uint8_t *buffer, int len); + void handle_ack_data_(uint8_t *buffer, int len); + void readline_(int readch, uint8_t *buffer, int len); + void query_parameters_(); + void get_version_(); + + uint16_t timeout_; + uint8_t max_move_distance_; + uint8_t max_still_distance_; + + uint8_t version_[6]; + uint8_t rg0_move_threshold_, rg0_still_threshold_, rg1_move_threshold_, rg1_still_threshold_, rg2_move_threshold_, + rg2_still_threshold_, rg3_move_threshold_, rg3_still_threshold_, rg4_move_threshold_, rg4_still_threshold_, + rg5_move_threshold_, rg5_still_threshold_, rg6_move_threshold_, rg6_still_threshold_, rg7_move_threshold_, + rg7_still_threshold_, rg8_move_threshold_, rg8_still_threshold_; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py new file mode 100644 index 0000000000..b941263134 --- /dev/null +++ b/esphome/components/ld2410/sensor.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ENERGY, + UNIT_CENTIMETER, + UNIT_PERCENT, +) +from . import CONF_LD2410_ID, LD2410Component + +DEPENDENCIES = ["ld2410"] +CONF_MOVING_DISTANCE = "moving_distance" +CONF_STILL_DISTANCE = "still_distance" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_ENERGY = "still_energy" +CONF_DETECTION_DISTANCE = "detection_distance" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT + ), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if CONF_MOVING_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE]) + cg.add(ld2410_component.set_moving_target_distance_sensor(sens)) + if CONF_STILL_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_STILL_DISTANCE]) + cg.add(ld2410_component.set_still_target_distance_sensor(sens)) + if CONF_MOVING_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_MOVING_ENERGY]) + cg.add(ld2410_component.set_moving_target_energy_sensor(sens)) + if CONF_STILL_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_STILL_ENERGY]) + cg.add(ld2410_component.set_still_target_energy_sensor(sens)) + if CONF_DETECTION_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_DETECTION_DISTANCE]) + cg.add(ld2410_component.set_detection_distance_sensor(sens)) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a56dccfd72..d6241dd5b0 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -6,17 +6,29 @@ #ifdef USE_ARDUINO #include #endif -#ifdef USE_ESP_IDF #include + +#define CLOCK_FREQUENCY 80e6f + +#ifdef USE_ARDUINO +#ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK +#undef CLOCK_FREQUENCY +// starting with ESP32 Arduino 2.0.2, the 40MHz crystal is used as clock by default if supported +#define CLOCK_FREQUENCY 40e6f #endif +#else +#define DEFAULT_CLK LEDC_USE_APB_CLK +#endif + +static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; namespace esphome { namespace ledc { static const char *const TAG = "ledc.output"; -#ifdef USE_ESP_IDF static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; +#ifdef USE_ESP_IDF #if SOC_LEDC_SUPPORT_HS_MODE // Only ESP32 has LEDC_HIGH_SPEED_MODE inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; } @@ -26,15 +38,13 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H // https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/ledc.html#functionality-overview inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif -#else -static const int MAX_RES_BITS = 20; #endif -float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); } +float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return CLOCK_FREQUENCY / float(1 << bit_depth); } float ledc_min_frequency_for_bit_depth(uint8_t bit_depth, bool low_frequency) { const float max_div_num = ((1 << MAX_RES_BITS) - 1) / (low_frequency ? 32.0f : 256.0f); - return 80e6f / (max_div_num * float(1 << bit_depth)); + return CLOCK_FREQUENCY / (max_div_num * float(1 << bit_depth)); } optional ledc_bit_depth_for_frequency(float frequency) { @@ -50,6 +60,38 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } +#ifdef USE_ESP_IDF +esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, + uint8_t channel, uint8_t &bit_depth, float frequency) { + bit_depth = *ledc_bit_depth_for_frequency(frequency); + if (bit_depth < 1) { + ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", frequency); + } + + ledc_timer_config_t timer_conf{}; + timer_conf.speed_mode = speed_mode; + timer_conf.duty_resolution = static_cast(bit_depth); + timer_conf.timer_num = timer_num; + timer_conf.freq_hz = (uint32_t) frequency; + timer_conf.clk_cfg = DEFAULT_CLK; + + // Configure the time with fallback in case of error + int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; + esp_err_t init_result = ESP_FAIL; + while (attempt_count_max > 0 && init_result != ESP_OK) { + init_result = ledc_timer_config(&timer_conf); + if (init_result != ESP_OK) { + ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth); + // try again with a lower bit depth + timer_conf.duty_resolution = static_cast(--bit_depth); + } + attempt_count_max--; + } + + return init_result; +} +#endif + void LEDCOutput::write_state(float state) { if (!initialized_) { ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); @@ -65,6 +107,7 @@ void LEDCOutput::write_state(float state) { auto duty = static_cast(duty_rounded); #ifdef USE_ARDUINO + ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_); ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF @@ -76,6 +119,7 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { + ESP_LOGV(TAG, "Entering setup..."); #ifdef USE_ARDUINO this->update_frequency(this->frequency_); this->turn_off(); @@ -87,19 +131,16 @@ void LEDCOutput::setup() { auto timer_num = static_cast((channel_ % 8) / 2); auto chan_num = static_cast(channel_ % 8); - bit_depth_ = *ledc_bit_depth_for_frequency(frequency_); - if (bit_depth_ < 1) { - ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency_); - this->status_set_warning(); + esp_err_t timer_init_result = + configure_timer_frequency(speed_mode, timer_num, chan_num, this->channel_, this->bit_depth_, this->frequency_); + + if (timer_init_result != ESP_OK) { + ESP_LOGE(TAG, "Frequency %f can't be achieved with computed bit depth %u", this->frequency_, this->bit_depth_); + this->status_set_error(); + return; } - ledc_timer_config_t timer_conf{}; - timer_conf.speed_mode = speed_mode; - timer_conf.duty_resolution = static_cast(bit_depth_); - timer_conf.timer_num = timer_num; - timer_conf.freq_hz = (uint32_t) frequency_; - timer_conf.clk_cfg = LEDC_AUTO_CLK; - ledc_timer_config(&timer_conf); + ESP_LOGV(TAG, "Configured frequency %f with a bit depth of %u bits", this->frequency_, this->bit_depth_); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = pin_->get_pin(); @@ -111,6 +152,7 @@ void LEDCOutput::setup() { chan_conf.hpoint = 0; ledc_channel_config(&chan_conf); initialized_ = true; + this->status_clear_error(); #endif } @@ -118,36 +160,80 @@ void LEDCOutput::dump_config() { ESP_LOGCONFIG(TAG, "LEDC Output:"); LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " PWM Frequency: %.1f Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " Bit depth: %u", this->bit_depth_); + ESP_LOGV(TAG, " Max frequency for bit depth: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_)); + ESP_LOGV(TAG, " Min frequency for bit depth: %f", + ledc_min_frequency_for_bit_depth(this->bit_depth_, (this->frequency_ < 100))); + ESP_LOGV(TAG, " Max frequency for bit depth-1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ - 1)); + ESP_LOGV(TAG, " Min frequency for bit depth-1: %f", + ledc_min_frequency_for_bit_depth(this->bit_depth_ - 1, (this->frequency_ < 100))); + ESP_LOGV(TAG, " Max frequency for bit depth+1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ + 1)); + ESP_LOGV(TAG, " Min frequency for bit depth+1: %f", + ledc_min_frequency_for_bit_depth(this->bit_depth_ + 1, (this->frequency_ < 100))); + ESP_LOGV(TAG, " Max res bits: %d", MAX_RES_BITS); + ESP_LOGV(TAG, " Clock frequency: %f", CLOCK_FREQUENCY); } void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { - ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); - this->status_set_warning(); + ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", this->frequency_); + this->status_set_error(); } this->bit_depth_ = bit_depth_opt.value_or(8); this->frequency_ = frequency; #ifdef USE_ARDUINO - ledcSetup(this->channel_, frequency, this->bit_depth_); - initialized_ = true; + ESP_LOGV(TAG, "Using Arduino API - Trying to define channel, frequency and bit depth..."); + u_int32_t configured_frequency = 0; + + // Configure LEDC channel, frequency and bit depth with fallback + int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; + while (attempt_count_max > 0 && configured_frequency == 0) { + ESP_LOGV(TAG, "Trying initialize channel %u with frequency %.1f and bit depth of %u...", this->channel_, + this->frequency_, this->bit_depth_); + configured_frequency = ledcSetup(this->channel_, frequency, this->bit_depth_); + if (configured_frequency != 0) { + initialized_ = true; + this->status_clear_error(); + ESP_LOGV(TAG, "Configured frequency: %u with bit depth: %u", configured_frequency, this->bit_depth_); + } else { + ESP_LOGW(TAG, "Unable to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, + this->frequency_, this->bit_depth_); + // try again with a lower bit depth + this->bit_depth_--; + } + attempt_count_max--; + } + + if (configured_frequency == 0) { + ESP_LOGE(TAG, "Permanently failed to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, + this->frequency_, this->bit_depth_); + this->status_set_error(); + return; + } + #endif // USE_ARDUINO #ifdef USE_ESP_IDF if (!initialized_) { ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!"); return; } + auto speed_mode = get_speed_mode(channel_); auto timer_num = static_cast((channel_ % 8) / 2); + auto chan_num = static_cast(channel_ % 8); - ledc_timer_config_t timer_conf{}; - timer_conf.speed_mode = speed_mode; - timer_conf.duty_resolution = static_cast(bit_depth_); - timer_conf.timer_num = timer_num; - timer_conf.freq_hz = (uint32_t) frequency_; - timer_conf.clk_cfg = LEDC_AUTO_CLK; - ledc_timer_config(&timer_conf); + esp_err_t timer_init_result = + configure_timer_frequency(speed_mode, timer_num, chan_num, this->channel_, this->bit_depth_, this->frequency_); + + if (timer_init_result != ESP_OK) { + ESP_LOGE(TAG, "Frequency %f can't be achieved with computed bit depth %u", this->frequency_, this->bit_depth_); + this->status_set_error(); + return; + } + + this->status_clear_error(); #endif // re-apply duty this->write_state(this->duty_); diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 00c03eb5f0..64c29a346b 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -276,7 +276,15 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot void LightState::save_remote_values_() { LightStateRTCState saved; saved.color_mode = this->remote_values.get_color_mode(); - saved.state = this->remote_values.is_on(); + switch (this->restore_mode_) { + case LIGHT_RESTORE_AND_OFF: + case LIGHT_RESTORE_AND_ON: + saved.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); + break; + default: + saved.state = this->remote_values.is_on(); + break; + } saved.brightness = this->remote_values.get_brightness(); saved.color_brightness = this->remote_values.get_color_brightness(); saved.red = this->remote_values.get_red(); diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py new file mode 100644 index 0000000000..1c549007b9 --- /dev/null +++ b/esphome/components/matrix_keypad/__init__.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import key_provider +from esphome.const import CONF_ID, CONF_PIN + +CODEOWNERS = ["@ssieb"] + +AUTO_LOAD = ["key_provider"] + +MULTI_CONF = True + +matrix_keypad_ns = cg.esphome_ns.namespace("matrix_keypad") +MatrixKeypad = matrix_keypad_ns.class_( + "MatrixKeypad", key_provider.KeyProvider, cg.Component +) + +CONF_KEYPAD_ID = "keypad_id" +CONF_ROWS = "rows" +CONF_COLUMNS = "columns" +CONF_KEYS = "keys" +CONF_DEBOUNCE_TIME = "debounce_time" +CONF_HAS_DIODES = "has_diodes" + + +def check_keys(obj): + if CONF_KEYS in obj: + if len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(obj[CONF_COLUMNS]): + raise cv.Invalid("The number of key codes must equal the number of buttons") + return obj + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MatrixKeypad), + cv.Required(CONF_ROWS): cv.All( + cv.ensure_list({cv.Required(CONF_PIN): pins.gpio_output_pin_schema}), + cv.Length(min=1), + ), + cv.Required(CONF_COLUMNS): cv.All( + cv.ensure_list({cv.Required(CONF_PIN): pins.gpio_input_pin_schema}), + cv.Length(min=1), + ), + cv.Optional(CONF_KEYS): cv.string, + cv.Optional(CONF_DEBOUNCE_TIME, default=1): cv.int_range(min=1, max=100), + cv.Optional(CONF_HAS_DIODES): cv.boolean, + } + ), + check_keys, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + row_pins = [] + for conf in config[CONF_ROWS]: + pin = await cg.gpio_pin_expression(conf[CONF_PIN]) + row_pins.append(pin) + cg.add(var.set_rows(row_pins)) + col_pins = [] + for conf in config[CONF_COLUMNS]: + pin = await cg.gpio_pin_expression(conf[CONF_PIN]) + col_pins.append(pin) + cg.add(var.set_columns(col_pins)) + if CONF_KEYS in config: + cg.add(var.set_keys(config[CONF_KEYS])) + cg.add(var.set_debounce_time(config[CONF_DEBOUNCE_TIME])) + if CONF_HAS_DIODES in config: + cg.add(var.set_has_diodes(config[CONF_HAS_DIODES])) diff --git a/esphome/components/matrix_keypad/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py new file mode 100644 index 0000000000..204db98650 --- /dev/null +++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID, CONF_KEY +from .. import MatrixKeypad, matrix_keypad_ns, CONF_KEYPAD_ID + +CONF_ROW = "row" +CONF_COL = "col" + +DEPENDENCIES = ["matrix_keypad"] + +MatrixKeypadBinarySensor = matrix_keypad_ns.class_( + "MatrixKeypadBinarySensor", binary_sensor.BinarySensor +) + + +def check_button(obj): + if CONF_ROW in obj or CONF_COL in obj: + if CONF_KEY in obj: + raise cv.Invalid("You can't provide both a key and a position") + if CONF_ROW not in obj: + raise cv.Invalid("Missing row") + if CONF_COL not in obj: + raise cv.Invalid("Missing col") + elif CONF_KEY not in obj: + raise cv.Invalid("Missing key or position") + elif len(obj[CONF_KEY]) != 1: + raise cv.Invalid("Key must be one character") + return obj + + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.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_, + cv.Optional(CONF_KEY): cv.string, + } + ), + check_button, +) + + +async def to_code(config): + if CONF_KEY in config: + var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY][0]) + else: + var = cg.new_Pvariable(config[CONF_ID], config[CONF_ROW], config[CONF_COL]) + await binary_sensor.register_binary_sensor(var, config) + matrix_keypad = await cg.get_variable(config[CONF_KEYPAD_ID]) + cg.add(matrix_keypad.register_listener(var)) diff --git a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h new file mode 100644 index 0000000000..d8a217f55e --- /dev/null +++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/components/matrix_keypad/matrix_keypad.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace matrix_keypad { + +class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensor { + public: + MatrixKeypadBinarySensor(uint8_t key) : has_key_(true), key_(key){}; + MatrixKeypadBinarySensor(const char *key) : has_key_(true), key_((uint8_t) key[0]){}; + MatrixKeypadBinarySensor(int row, int col) : has_key_(false), row_(row), col_(col){}; + + void key_pressed(uint8_t key) override { + if (!this->has_key_) + return; + if (key == this->key_) + this->publish_state(true); + } + + void key_released(uint8_t key) override { + if (!this->has_key_) + return; + if (key == this->key_) + this->publish_state(false); + } + + void button_pressed(int row, int col) override { + if (this->has_key_) + return; + if ((row == this->row_) && (col == this->col_)) + this->publish_state(true); + } + + void button_released(int row, int col) override { + if (this->has_key_) + return; + if ((row == this->row_) && (col == this->col_)) + this->publish_state(false); + } + + protected: + bool has_key_; + uint8_t key_; + int row_; + int col_; +}; + +} // namespace matrix_keypad +} // namespace esphome diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp new file mode 100644 index 0000000000..f4e7bf4d23 --- /dev/null +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -0,0 +1,102 @@ +#include "matrix_keypad.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace matrix_keypad { + +static const char *const TAG = "matrix_keypad"; + +void MatrixKeypad::setup() { + for (auto *pin : this->rows_) { + if (!has_diodes_) { + pin->pin_mode(gpio::FLAG_INPUT); + } else { + pin->digital_write(true); + } + } + for (auto *pin : this->columns_) + pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); +} + +void MatrixKeypad::loop() { + static uint32_t active_start = 0; + static int active_key = -1; + uint32_t now = millis(); + int key = -1; + bool error = false; + int pos = 0, row, col; + for (auto *row : this->rows_) { + if (!has_diodes_) + row->pin_mode(gpio::FLAG_OUTPUT); + row->digital_write(false); + for (auto *col : this->columns_) { + if (!col->digital_read()) { + if (key != -1) { + error = true; + } else { + key = pos; + } + } + pos++; + } + row->digital_write(true); + if (!has_diodes_) + row->pin_mode(gpio::FLAG_INPUT); + } + if (error) + return; + + if (key != active_key) { + if ((active_key != -1) && (this->pressed_key_ == active_key)) { + row = this->pressed_key_ / this->columns_.size(); + col = this->pressed_key_ % this->columns_.size(); + ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); + for (auto &listener : this->listeners_) + listener->button_released(row, col); + if (!this->keys_.empty()) { + uint8_t keycode = this->keys_[this->pressed_key_]; + ESP_LOGD(TAG, "key '%c' released", keycode); + for (auto &listener : this->listeners_) + listener->key_released(keycode); + } + this->pressed_key_ = -1; + } + + active_key = key; + if (key == -1) + return; + active_start = now; + } + + if ((this->pressed_key_ == key) || (now - active_start < this->debounce_time_)) + return; + + row = key / this->columns_.size(); + col = key % this->columns_.size(); + ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col); + for (auto &listener : this->listeners_) + listener->button_pressed(row, col); + if (!this->keys_.empty()) { + uint8_t keycode = this->keys_[key]; + ESP_LOGD(TAG, "key '%c' pressed", keycode); + for (auto &listener : this->listeners_) + listener->key_pressed(keycode); + this->send_key_(keycode); + } + this->pressed_key_ = key; +} + +void MatrixKeypad::dump_config() { + ESP_LOGCONFIG(TAG, "Matrix Keypad:"); + ESP_LOGCONFIG(TAG, " Rows:"); + for (auto &pin : this->rows_) + LOG_PIN(" Pin: ", pin); + ESP_LOGCONFIG(TAG, " Cols:"); + for (auto &pin : this->columns_) + LOG_PIN(" Pin: ", pin); +} + +void MatrixKeypad::register_listener(MatrixKeypadListener *listener) { this->listeners_.push_back(listener); } + +} // namespace matrix_keypad +} // namespace esphome diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h new file mode 100644 index 0000000000..9f5942be9a --- /dev/null +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome { +namespace matrix_keypad { + +class MatrixKeypadListener { + public: + virtual void button_pressed(int row, int col){}; + virtual void button_released(int row, int col){}; + virtual void key_pressed(uint8_t key){}; + virtual void key_released(uint8_t key){}; +}; + +class MatrixKeypad : public key_provider::KeyProvider, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + void set_columns(std::vector pins) { columns_ = std::move(pins); }; + void set_rows(std::vector pins) { rows_ = std::move(pins); }; + void set_keys(std::string keys) { keys_ = std::move(keys); }; + void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; + void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; + + void register_listener(MatrixKeypadListener *listener); + + protected: + std::vector rows_; + std::vector columns_; + std::string keys_; + int debounce_time_ = 0; + bool has_diodes_{false}; + int pressed_key_ = -1; + + std::vector listeners_{}; +}; + +} // namespace matrix_keypad +} // namespace esphome diff --git a/esphome/components/mcp9600/sensor.py b/esphome/components/mcp9600/sensor.py index 4c10df2dab..392ee4e773 100644 --- a/esphome/components/mcp9600/sensor.py +++ b/esphome/components/mcp9600/sensor.py @@ -58,7 +58,7 @@ CONFIG_SCHEMA = ( ) FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema( - "mcp9600", min_frequency="100khz" + "mcp9600", min_frequency="10khz" ) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 154e6b099f..e27786a98b 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,4 +1,10 @@ -from esphome.const import CONF_ID +from esphome.const import ( + CONF_ID, + CONF_PORT, + CONF_PROTOCOL, + CONF_SERVICES, + CONF_SERVICE, +) import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE, coroutine_with_priority @@ -8,6 +14,8 @@ DEPENDENCIES = ["network"] mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) +MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord") +MDNSService = mdns_ns.struct("MDNSService") def _remove_id_if_disabled(value): @@ -17,18 +25,50 @@ def _remove_id_if_disabled(value): return value +CONF_TXT = "txt" + +SERVICE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SERVICE): cv.string, + cv.Required(CONF_PROTOCOL): cv.string, + cv.Optional(CONF_PORT, default=0): cv.Any(0, cv.port), + cv.Optional(CONF_TXT, default={}): {cv.string: cv.string}, + } +) + CONF_DISABLED = "disabled" CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(MDNSComponent), cv.Optional(CONF_DISABLED, default=False): cv.boolean, + cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), } ), _remove_id_if_disabled, ) +def mdns_txt_record(key: str, value: str): + return cg.StructInitializer( + MDNSTXTRecord, + ("key", key), + ("value", value), + ) + + +def mdns_service( + service: str, proto: str, port: int, txt_records: list[dict[str, str]] +): + return cg.StructInitializer( + MDNSService, + ("service_type", service), + ("proto", proto), + ("port", port), + ("txt_records", txt_records), + ) + + @coroutine_with_priority(55.0) async def to_code(config): if CORE.using_arduino: @@ -46,3 +86,15 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + for service in config[CONF_SERVICES]: + txt = [ + mdns_txt_record(txt_key, txt_value) + for txt_key, txt_value in service[CONF_TXT].items() + ] + + exp = mdns_service( + service[CONF_SERVICE], service[CONF_PROTOCOL], service[CONF_PORT], txt + ) + + cg.add(var.add_extra_service(exp)) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index a3f38322b3..cdb9aa8e74 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -30,6 +30,9 @@ void MDNSComponent::compile_records_() { service.service_type = "_esphomelib"; service.proto = "_tcp"; service.port = api::global_api_server->get_port(); + if (!App.get_friendly_name().empty()) { + service.txt_records.push_back({"friendly_name", App.get_friendly_name()}); + } service.txt_records.push_back({"version", ESPHOME_VERSION}); service.txt_records.push_back({"mac", get_mac_address()}); const char *platform = nullptr; @@ -87,6 +90,8 @@ void MDNSComponent::compile_records_() { } #endif + this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end()); + if (this->services_.empty()) { // Publish "http" service if not using native API // This is just to have *some* mDNS service so that .local resolution works diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index dbf7482f1f..b2cb10db62 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -33,7 +33,12 @@ class MDNSComponent : public Component { #endif float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } + + void on_shutdown() override; + protected: + std::vector services_extra_{}; std::vector services_{}; std::string hostname_; void compile_records_(); diff --git a/esphome/components/mdns/mdns_esp_idf.cpp b/esphome/components/mdns/mdns_esp32.cpp similarity index 87% rename from esphome/components/mdns/mdns_esp_idf.cpp rename to esphome/components/mdns/mdns_esp32.cpp index 40d9f1d5f3..6081c96637 100644 --- a/esphome/components/mdns/mdns_esp_idf.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -1,9 +1,10 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 -#include "mdns_component.h" -#include "esphome/core/log.h" #include #include +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "mdns_component.h" namespace esphome { namespace mdns { @@ -47,7 +48,12 @@ void MDNSComponent::setup() { } } +void MDNSComponent::on_shutdown() { + mdns_free(); + delay(40); // Allow the mdns packets announcing service removal to be sent +} + } // namespace mdns } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/mdns/mdns_esp32_arduino.cpp b/esphome/components/mdns/mdns_esp32_arduino.cpp deleted file mode 100644 index 6a66beef92..0000000000 --- a/esphome/components/mdns/mdns_esp32_arduino.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include "mdns_component.h" -#include "esphome/core/log.h" -#include - -namespace esphome { -namespace mdns { - -void MDNSComponent::setup() { - this->compile_records_(); - - MDNS.begin(this->hostname_.c_str()); - - for (const auto &service : this->services_) { - MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port); - for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str()); - } - } -} - -} // namespace mdns -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index b91f60cb6b..4ccfe42baa 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -1,10 +1,11 @@ #if defined(USE_ESP8266) && defined(USE_ARDUINO) -#include "mdns_component.h" -#include "esphome/core/log.h" +#include #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" -#include +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "mdns_component.h" namespace esphome { namespace mdns { @@ -37,6 +38,11 @@ void MDNSComponent::setup() { void MDNSComponent::loop() { MDNS.update(); } +void MDNSComponent::on_shutdown() { + MDNS.close(); + delay(10); +} + } // namespace mdns } // namespace esphome diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index e443a78c48..b30a3a4ee7 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -38,6 +38,11 @@ void MDNSComponent::setup() { void MDNSComponent::loop() { MDNS.update(); } +void MDNSComponent::on_shutdown() { + MDNS.close(); + delay(40); +} + } // namespace mdns } // namespace esphome diff --git a/esphome/components/mics_4514/__init__.py b/esphome/components/mics_4514/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp new file mode 100644 index 0000000000..a14d7f2f80 --- /dev/null +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -0,0 +1,145 @@ +#include "mics_4514.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mics_4514 { + +static const char *const TAG = "mics_4514"; + +static const uint8_t SENSOR_REGISTER = 0x04; +static const uint8_t POWER_MODE_REGISTER = 0x0a; + +void MICS4514Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MICS 4514..."); + uint8_t power_mode; + this->read_register(POWER_MODE_REGISTER, &power_mode, 1); + if (power_mode == 0x00) { + ESP_LOGCONFIG(TAG, "Waking up MICS 4514, sensors will have data after 3 minutes..."); + power_mode = 0x01; + this->write_register(POWER_MODE_REGISTER, &power_mode, 1); + delay(100); // NOLINT + this->set_timeout("warmup", 3 * 60 * 1000, [this]() { + this->warmed_up_ = true; + ESP_LOGCONFIG(TAG, "MICS 4514 setup complete."); + }); + this->status_set_warning(); + return; + } + ESP_LOGCONFIG(TAG, "Device already awake."); + this->warmed_up_ = true; + ESP_LOGCONFIG(TAG, "MICS 4514 setup complete."); +} +void MICS4514Component::dump_config() { + ESP_LOGCONFIG(TAG, "MICS 4514:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_); + LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_); + LOG_SENSOR(" ", "Methane", this->methane_sensor_); + LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_); + LOG_SENSOR(" ", "Hydrogen", this->hydrogen_sensor_); + LOG_SENSOR(" ", "Ammonia", this->ammonia_sensor_); +} +float MICS4514Component::get_setup_priority() const { return setup_priority::DATA; } +void MICS4514Component::update() { + if (!this->warmed_up_) { + return; + } + uint8_t data[6]; + if (this->read_register(SENSOR_REGISTER, data, 6) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + this->status_clear_warning(); + ESP_LOGV(TAG, "Got data: %02X %02X %02X %02X %02X %02X", data[0], data[1], data[2], data[3], data[4], data[5]); + uint16_t ox = encode_uint16(data[0], data[1]); + uint16_t red = encode_uint16(data[2], data[3]); + uint16_t power = encode_uint16(data[4], data[5]); + + if (this->initial_) { + this->initial_ = false; + this->ox_calibration_ = (float) (power - ox); + this->red_calibration_ = (float) (power - red); + return; + } + + float red_f = (float) (power - red) / this->red_calibration_; + float ox_f = (float) (power - ox) / this->ox_calibration_; + + if (this->carbon_monoxide_sensor_ != nullptr) { + float co = 0.0f; + if (red_f <= 0.425f) { + co = (0.425f - red_f) / 0.000405f; + if (co < 1.0f) + co = 0.0f; + if (co > 1000.0f) + co = 1000.0f; + } + this->carbon_monoxide_sensor_->publish_state(co); + } + + if (this->nitrogen_dioxide_sensor_ != nullptr) { + float nitrogendioxide = 0.0f; + if (ox_f >= 1.1f) { + nitrogendioxide = (ox_f - 0.045f) / 6.13f; + if (nitrogendioxide < 0.1f) + nitrogendioxide = 0.0f; + if (nitrogendioxide > 10.0f) + nitrogendioxide = 10.0f; + } + this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide); + } + + if (this->methane_sensor_ != nullptr) { + float methane = 0.0f; + if (red_f <= 0.786f) { + methane = (0.786f - red_f) / 0.000023f; + if (methane < 1000.0f) + methane = 0.0f; + if (methane > 25000.0f) + methane = 25000.0f; + } + this->methane_sensor_->publish_state(methane); + } + + if (this->ethanol_sensor_ != nullptr) { + float ethanol = 0.0f; + if (red_f <= 0.306f) { + ethanol = (0.306f - red_f) / 0.00057f; + if (ethanol < 10.0f) + ethanol = 0.0f; + if (ethanol > 500.0f) + ethanol = 500.0f; + } + this->ethanol_sensor_->publish_state(ethanol); + } + + if (this->hydrogen_sensor_ != nullptr) { + float hydrogen = 0.0f; + if (red_f <= 0.279f) { + hydrogen = (0.279f - red_f) / 0.00026f; + if (hydrogen < 1.0f) + hydrogen = 0.0f; + if (hydrogen > 1000.0f) + hydrogen = 1000.0f; + } + this->hydrogen_sensor_->publish_state(hydrogen); + } + + if (this->ammonia_sensor_ != nullptr) { + float ammonia = 0.0f; + if (red_f <= 0.8f) { + ammonia = (0.8f - red_f) / 0.0015f; + if (ammonia < 1.0f) + ammonia = 0.0f; + if (ammonia > 500.0f) + ammonia = 500.0f; + } + this->ammonia_sensor_->publish_state(ammonia); + } +} + +} // namespace mics_4514 +} // namespace esphome diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h new file mode 100644 index 0000000000..d2fefc3630 --- /dev/null +++ b/esphome/components/mics_4514/mics_4514.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace mics_4514 { + +class MICS4514Component : public PollingComponent, public i2c::I2CDevice { + SUB_SENSOR(carbon_monoxide) + SUB_SENSOR(nitrogen_dioxide) + SUB_SENSOR(methane) + SUB_SENSOR(ethanol) + SUB_SENSOR(hydrogen) + SUB_SENSOR(ammonia) + + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + bool warmed_up_{false}; + bool initial_{true}; + + float ox_calibration_{0}; + float red_calibration_{0}; +}; + +} // namespace mics_4514 +} // namespace esphome diff --git a/esphome/components/mics_4514/sensor.py b/esphome/components/mics_4514/sensor.py new file mode 100644 index 0000000000..80c3524f66 --- /dev/null +++ b/esphome/components/mics_4514/sensor.py @@ -0,0 +1,71 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.components import sensor, i2c + +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +CONF_CARBON_MONOXIDE = "carbon_monoxide" +CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" +CONF_METHANE = "methane" +CONF_ETHANOL = "ethanol" +CONF_HYDROGEN = "hydrogen" +CONF_AMMONIA = "ammonia" + + +mics_4514_ns = cg.esphome_ns.namespace("mics_4514") +MICS4514Component = mics_4514_ns.class_( + "MICS4514Component", cg.PollingComponent, i2c.I2CDevice +) + +SENSORS = [ + CONF_CARBON_MONOXIDE, + CONF_METHANE, + CONF_ETHANOL, + CONF_HYDROGEN, + CONF_AMMONIA, +] + +common_sensor_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MICS4514Component), + cv.Optional(CONF_NITROGEN_DIOXIDE): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + ), + } + ) + .extend({cv.Optional(sensor_type): common_sensor_schema for sensor_type in SENSORS}) + .extend(i2c.i2c_device_schema(0x75)) + .extend(cv.polling_component_schema("60s")) +) + + +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) + + for sensor_type in SENSORS: + if sensor_type in config: + sens = await sensor.new_sensor(config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor")(sens)) + + if CONF_NITROGEN_DIOXIDE in config: + sens = await sensor.new_sensor(config[CONF_NITROGEN_DIOXIDE]) + cg.add(var.set_nitrogen_dioxide_sensor(sens)) diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 1ad5ade53d..b5bf43b64f 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -84,18 +84,18 @@ ClimateTraits AirConditioner::traits() { traits.set_supported_custom_presets(this->supported_custom_presets_); traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); /* + MINIMAL SET OF CAPABILITIES */ - traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); - traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); - traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); - traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); - traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); - traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) Converters::to_climate_traits(traits, this->base_.getCapabilities()); + if (!traits.get_supported_modes().empty()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); + if (!traits.get_supported_swing_modes().empty()) + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); + if (!traits.get_supported_presets().empty()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); return traits; } diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 254322d097..6fea7033f2 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_FLOW_CONTROL_PIN, CONF_ID, CONF_ADDRESS, + CONF_DISABLE_CRC, ) from esphome import pins @@ -27,6 +28,7 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_SEND_WAIT_TIME, default="250ms" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean, } ) .extend(cv.COMPONENT_SCHEMA) @@ -45,8 +47,8 @@ async def to_code(config): pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) cg.add(var.set_flow_control_pin(pin)) - if CONF_SEND_WAIT_TIME in config: - cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME])) + cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME])) + cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC])) def modbus_device_schema(default_address): diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 4d75675d0f..137fb0b26b 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -102,8 +102,12 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint16_t computed_crc = crc16(raw, data_offset + data_len); uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); if (computed_crc != remote_crc) { - ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); - return false; + if (this->disable_crc_) { + ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc); + } else { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } } } std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); @@ -139,6 +143,7 @@ void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:"); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); ESP_LOGCONFIG(TAG, " Send Wait Time: %d ms", this->send_wait_time_); + ESP_LOGCONFIG(TAG, " CRC Disabled: %s", YESNO(this->disable_crc_)); } float Modbus::get_setup_priority() const { // After UART bus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 95414ba090..dd8732c6e9 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -30,12 +30,14 @@ class Modbus : public uart::UARTDevice, public Component { void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } uint8_t waiting_for_response{0}; void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; } + void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; } protected: GPIOPin *flow_control_pin_{nullptr}; bool parse_modbus_byte_(uint8_t byte); uint16_t send_wait_time_{250}; + bool disable_crc_; std::vector rx_buffer_; uint32_t last_modbus_byte_{0}; uint32_t last_send_{0}; diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index fc57318a81..60592b00ad 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -53,6 +53,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Now parse the data - See Datasheet for definition if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP && + static_cast(manu_data.data[0]) != LIPPERT_BOTTOM_UP && static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP) { ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); return false; diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index e6cc1fd6f1..8b126a204c 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -15,6 +15,7 @@ enum SensorType { STANDARD_BOTTOM_UP = 0x03, TOP_DOWN_AIR_ABOVE = 0x04, BOTTOM_UP_WATER = 0x05, + LIPPERT_BOTTOM_UP = 0x06, PLUS_BOTTOM_UP = 0x08 // all other values are reserved }; diff --git a/esphome/components/mqtt/mqtt_backend_idf.cpp b/esphome/components/mqtt/mqtt_backend_idf.cpp index 0726f72567..812e36d522 100644 --- a/esphome/components/mqtt/mqtt_backend_idf.cpp +++ b/esphome/components/mqtt/mqtt_backend_idf.cpp @@ -69,7 +69,7 @@ void MQTTBackendIDF::loop() { } } -void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) { +void MQTTBackendIDF::mqtt_event_handler_(const Event &event) { ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id); switch (event.event_id) { case MQTT_EVENT_BEFORE_CONNECT: @@ -104,28 +104,24 @@ void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) { break; case MQTT_EVENT_DATA: { static std::string topic; - if (event.topic) { - // not 0 terminated - create a string from it - topic = std::string(event.topic, event.topic_len); + if (event.topic.length() > 0) { + topic = event.topic; } ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); - auto data_len = event.data_len; - if (data_len == 0) - data_len = strlen(event.data); - this->on_message_.call(event.topic ? const_cast(topic.c_str()) : nullptr, event.data, data_len, + this->on_message_.call(event.topic.length() > 0 ? topic.c_str() : nullptr, event.data.data(), event.data.size(), event.current_data_offset, event.total_data_len); } break; case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); - if (event.error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { - ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle->esp_tls_last_esp_err); - ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle->esp_tls_stack_err); - ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle->esp_transport_sock_errno, - strerror(event.error_handle->esp_transport_sock_errno)); - } else if (event.error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { - ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle->connect_return_code); + if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle.esp_tls_last_esp_err); + ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle.esp_tls_stack_err); + ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle.esp_transport_sock_errno, + strerror(event.error_handle.esp_transport_sock_errno)); + } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { + ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle.connect_return_code); } else { - ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle->error_type); + ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle.error_type); } break; default: @@ -140,7 +136,7 @@ void MQTTBackendIDF::mqtt_event_handler(void *handler_args, esp_event_base_t bas // queue event to decouple processing if (instance) { auto event = *static_cast(event_data); - instance->mqtt_events_.push(event); + instance->mqtt_events_.push(Event(event)); } } diff --git a/esphome/components/mqtt/mqtt_backend_idf.h b/esphome/components/mqtt/mqtt_backend_idf.h index 77b5592d72..900ee9709b 100644 --- a/esphome/components/mqtt/mqtt_backend_idf.h +++ b/esphome/components/mqtt/mqtt_backend_idf.h @@ -12,6 +12,33 @@ namespace esphome { namespace mqtt { +struct Event { + esp_mqtt_event_id_t event_id; + std::vector data; + int total_data_len; + int current_data_offset; + std::string topic; + int msg_id; + bool retain; + int qos; + bool dup; + esp_mqtt_error_codes_t error_handle; + + // Construct from esp_mqtt_event_t + // Any pointer values that are unsafe to keep are converted to safe copies + Event(const esp_mqtt_event_t &event) + : event_id(event.event_id), + data(event.data, event.data + event.data_len), + total_data_len(event.total_data_len), + current_data_offset(event.current_data_offset), + topic(event.topic, event.topic_len), + msg_id(event.msg_id), + retain(event.retain), + qos(event.qos), + dup(event.dup), + error_handle(*event.error_handle) {} +}; + class MQTTBackendIDF final : public MQTTBackend { public: static const size_t MQTT_BUFFER_SIZE = 4096; @@ -99,7 +126,7 @@ class MQTTBackendIDF final : public MQTTBackend { protected: bool initialize_(); - void mqtt_event_handler_(const esp_mqtt_event_t &event); + void mqtt_event_handler_(const Event &event); static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); struct MqttClientDeleter { @@ -134,7 +161,7 @@ class MQTTBackendIDF final : public MQTTBackend { CallbackManager on_unsubscribe_; CallbackManager on_message_; CallbackManager on_publish_; - std::queue mqtt_events_; + std::queue mqtt_events_; }; } // namespace mqtt diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 7c3c414b3a..8dd03dd5c8 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -66,18 +66,42 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; - if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { - // away_mode_command_topic - root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic(); - // away_mode_state_topic - root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic(); + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { + // preset_mode_command_topic + root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic(); + // preset_mode_state_topic + root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); + // presets + JsonArray presets = root.createNestedArray("presets"); + if (traits.supports_preset(CLIMATE_PRESET_HOME)) + presets.add("home"); + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { + // away_mode_command_topic + root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic(); + // away_mode_state_topic + root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic(); + presets.add("away"); + } + if (traits.supports_preset(CLIMATE_PRESET_BOOST)) + presets.add("boost"); + if (traits.supports_preset(CLIMATE_PRESET_COMFORT)) + presets.add("comfort"); + if (traits.supports_preset(CLIMATE_PRESET_ECO)) + presets.add("eco"); + if (traits.supports_preset(CLIMATE_PRESET_SLEEP)) + presets.add("sleep"); + if (traits.supports_preset(CLIMATE_PRESET_ACTIVITY)) + presets.add("activity"); + for (const auto &preset : traits.get_supported_custom_presets()) + presets.add(preset); } + if (traits.get_supports_action()) { // action_topic root[MQTT_ACTION_TOPIC] = this->get_action_state_topic(); } - if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { + if (traits.get_supports_fan_modes()) { // fan_mode_command_topic root[MQTT_FAN_MODE_COMMAND_TOPIC] = this->get_fan_mode_command_topic(); // fan_mode_state_topic @@ -102,6 +126,8 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo fan_modes.add("focus"); if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE)) fan_modes.add("diffuse"); + if (traits.supports_fan_mode(CLIMATE_FAN_QUIET)) + fan_modes.add("quiet"); for (const auto &fan_mode : traits.get_supported_custom_fan_modes()) fan_modes.add(fan_mode); } @@ -194,6 +220,14 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { + this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto call = this->device_->make_call(); + call.set_preset(payload); + call.perform(); + }); + } + if (traits.get_supports_fan_modes()) { this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) { auto call = this->device_->make_call(); @@ -271,6 +305,42 @@ bool MQTTClimateComponent::publish_state_() { if (!this->publish(this->get_away_state_topic(), payload)) success = false; } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { + std::string payload; + if (this->device_->preset.has_value()) { + switch (this->device_->preset.value()) { + case CLIMATE_PRESET_NONE: + payload = "none"; + break; + case CLIMATE_PRESET_HOME: + payload = "home"; + break; + case CLIMATE_PRESET_AWAY: + payload = "away"; + break; + case CLIMATE_PRESET_BOOST: + payload = "boost"; + break; + case CLIMATE_PRESET_COMFORT: + payload = "comfort"; + break; + case CLIMATE_PRESET_ECO: + payload = "eco"; + break; + case CLIMATE_PRESET_SLEEP: + payload = "sleep"; + break; + case CLIMATE_PRESET_ACTIVITY: + payload = "activity"; + break; + } + } + if (this->device_->custom_preset.has_value()) + payload = this->device_->custom_preset.value(); + if (!this->publish(this->get_preset_state_topic(), payload)) + success = false; + } + if (traits.get_supports_action()) { const char *payload = "unknown"; switch (this->device_->action) { @@ -328,6 +398,9 @@ bool MQTTClimateComponent::publish_state_() { case CLIMATE_FAN_DIFFUSE: payload = "diffuse"; break; + case CLIMATE_FAN_QUIET: + payload = "quiet"; + break; } } if (this->device_->custom_fan_mode.has_value()) diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index ea3e2ab3fa..a93070fe66 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -35,6 +35,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(fan_mode, command) MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, state) MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, command) + MQTT_COMPONENT_CUSTOM_TOPIC(preset, state) + MQTT_COMPONENT_CUSTOM_TOPIC(preset, command) protected: const EntityBase *get_entity() const override; diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index ec4dead42e..463557e3b3 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -41,6 +41,7 @@ from esphome.const import ( DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, @@ -84,6 +85,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PM25, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_POWER, + DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 8392008222..98483e8ae9 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -165,7 +165,7 @@ def do_packages_pass(config: dict): f"Packages must be a key to value mapping, got {type(packages)} instead" ) - for package_name, package_config in packages.items(): + for package_name, package_config in reversed(packages.items()): with cv.prepend_path(package_name): recursive_package = package_config if CONF_URL in package_config: diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py new file mode 100644 index 0000000000..76d6ddaf32 --- /dev/null +++ b/esphome/components/pca9554/__init__.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, +) + +CODEOWNERS = ["@hwstar"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +pca9554_ns = cg.esphome_ns.namespace("pca9554") + +PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice) +PCA9554GPIOPin = pca9554_ns.class_( + "PCA9554GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9554Component) +) + +CONF_PCA9554 = "pca9554" +CONFIG_SCHEMA = ( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA9554Component)}) + .extend(cv.COMPONENT_SCHEMA) + .extend( + i2c.i2c_device_schema(0x20) + ) # Note: 0x20 for the non-A part. The PCA9554A parts start at addess 0x38 +) + + +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) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +PCA9554_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), + cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=8), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register("pca9554", PCA9554_PIN_SCHEMA) +async def pca9554_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_PCA9554]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp new file mode 100644 index 0000000000..39093fcf54 --- /dev/null +++ b/esphome/components/pca9554/pca9554.cpp @@ -0,0 +1,112 @@ +#include "pca9554.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pca9554 { + +const uint8_t INPUT_REG = 0; +const uint8_t OUTPUT_REG = 1; +const uint8_t INVERT_REG = 2; +const uint8_t CONFIG_REG = 3; + +static const char *const TAG = "pca9554"; + +void PCA9554Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A..."); + // Test to see if device exists + if (!this->read_inputs_()) { + ESP_LOGE(TAG, "PCA9554 not available under 0x%02X", this->address_); + this->mark_failed(); + return; + } + + // No polarity inversion + this->write_register_(INVERT_REG, 0); + // All inputs at initialization + this->config_mask_ = 0; + // Invert mask as the part sees a 1 as an input + this->write_register_(CONFIG_REG, ~this->config_mask_); + // All ouputs low + this->output_mask_ = 0; + this->write_register_(OUTPUT_REG, this->output_mask_); + // Read the inputs + this->read_inputs_(); + ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} +void PCA9554Component::dump_config() { + ESP_LOGCONFIG(TAG, "PCA9554:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCA9554 failed!"); + } +} + +bool PCA9554Component::digital_read(uint8_t pin) { + this->read_inputs_(); + return this->input_mask_ & (1 << pin); +} + +void PCA9554Component::digital_write(uint8_t pin, bool value) { + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + this->write_register_(OUTPUT_REG, this->output_mask_); +} + +void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + // Clear mode mask bit + this->config_mask_ &= ~(1 << pin); + } else if (flags == gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->config_mask_ |= 1 << pin; + } + this->write_register_(CONFIG_REG, ~this->config_mask_); +} + +bool PCA9554Component::read_inputs_() { + uint8_t inputs; + + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->read_register(INPUT_REG, &inputs, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + this->status_clear_warning(); + this->input_mask_ = inputs; + return true; +} + +bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { + if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } + +void PCA9554GPIOPin::setup() { pin_mode(flags_); } +void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string PCA9554GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via PCA9554", pin_); + return buffer; +} + +} // namespace pca9554 +} // namespace esphome diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h new file mode 100644 index 0000000000..d1bfc36bec --- /dev/null +++ b/esphome/components/pca9554/pca9554.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pca9554 { + +class PCA9554Component : public Component, public i2c::I2CDevice { + public: + PCA9554Component() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + protected: + bool read_inputs_(); + + bool write_register_(uint8_t reg, uint8_t value); + + /// Mask for the pin config - 1 means OUTPUT, 0 means INPUT + uint8_t config_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_mask_{0x00}; + /// The state of the actual input pin states - 1 means HIGH, 0 means LOW + uint8_t input_mask_{0x00}; + /// Storage for last I2C error seen + esphome::i2c::ErrorCode last_error_; +}; + +/// Helper class to expose a PCA9554 pin as an internal input GPIO pin. +class PCA9554GPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(PCA9554Component *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + PCA9554Component *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pca9554 +} // namespace esphome diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py index 1a5ccc3247..b22577bf9f 100644 --- a/esphome/components/pca9685/__init__.py +++ b/esphome/components/pca9685/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c -from esphome.const import CONF_FREQUENCY, CONF_ID +from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_EXTERNAL_CLOCK_INPUT DEPENDENCIES = ["i2c"] MULTI_CONF = True @@ -9,21 +9,39 @@ MULTI_CONF = True pca9685_ns = cg.esphome_ns.namespace("pca9685") PCA9685Output = pca9685_ns.class_("PCA9685Output", cg.Component, i2c.I2CDevice) -CONFIG_SCHEMA = ( + +def validate_frequency(config): + if config[CONF_EXTERNAL_CLOCK_INPUT]: + if CONF_FREQUENCY in config: + raise cv.Invalid( + "Frequency cannot be set when using an external clock input" + ) + return config + if CONF_FREQUENCY not in config: + raise cv.Invalid("Frequency is required") + return config + + +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(PCA9685Output), - cv.Required(CONF_FREQUENCY): cv.All( + cv.Optional(CONF_FREQUENCY): cv.All( cv.frequency, cv.Range(min=23.84, max=1525.88) ), + cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean, } ) .extend(cv.COMPONENT_SCHEMA) - .extend(i2c.i2c_device_schema(0x40)) + .extend(i2c.i2c_device_schema(0x40)), + validate_frequency, ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID], config[CONF_FREQUENCY]) + var = cg.new_Pvariable(config[CONF_ID]) + if CONF_FREQUENCY in config: + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + cg.add(var.set_extclk(config[CONF_EXTERNAL_CLOCK_INPUT])) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 957f4062fc..c61251b66f 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -21,6 +21,7 @@ static const uint8_t PCA9685_REGISTER_LED0 = 0x06; static const uint8_t PCA9685_REGISTER_PRE_SCALE = 0xFE; static const uint8_t PCA9685_MODE1_RESTART = 0b10000000; +static const uint8_t PCA9685_MODE1_EXTCLK = 0b01000000; static const uint8_t PCA9685_MODE1_AUTOINC = 0b00100000; static const uint8_t PCA9685_MODE1_SLEEP = 0b00010000; @@ -28,10 +29,13 @@ void PCA9685Output::setup() { ESP_LOGCONFIG(TAG, "Setting up PCA9685OutputComponent..."); ESP_LOGV(TAG, " Resetting devices..."); + uint8_t address_tmp = this->address_; + this->set_i2c_address(0x00); if (!this->write_bytes(PCA9685_REGISTER_SOFTWARE_RESET, nullptr, 0)) { this->mark_failed(); return; } + this->set_i2c_address(address_tmp); if (!this->write_byte(PCA9685_REGISTER_MODE1, PCA9685_MODE1_RESTART | PCA9685_MODE1_AUTOINC)) { this->mark_failed(); @@ -42,14 +46,6 @@ void PCA9685Output::setup() { return; } - int pre_scaler = static_cast((25000000 / (4096 * this->frequency_)) - 1); - if (pre_scaler > 255) - pre_scaler = 255; - if (pre_scaler < 3) - pre_scaler = 3; - - ESP_LOGV(TAG, " -> Prescaler: %d", pre_scaler); - uint8_t mode1; if (!this->read_byte(PCA9685_REGISTER_MODE1, &mode1)) { this->mark_failed(); @@ -60,6 +56,20 @@ void PCA9685Output::setup() { this->mark_failed(); return; } + + int pre_scaler = 3; + if (this->extclk_) { + mode1 = mode1 | PCA9685_MODE1_EXTCLK; + if (!this->write_byte(PCA9685_REGISTER_MODE1, mode1)) { + this->mark_failed(); + return; + } + } else { + pre_scaler = static_cast((25000000 / (4096 * this->frequency_)) - 1); + pre_scaler = clamp(pre_scaler, 3, 255); + + ESP_LOGV(TAG, " -> Prescaler: %d", pre_scaler); + } if (!this->write_byte(PCA9685_REGISTER_PRE_SCALE, pre_scaler)) { this->mark_failed(); return; @@ -78,7 +88,12 @@ void PCA9685Output::setup() { void PCA9685Output::dump_config() { ESP_LOGCONFIG(TAG, "PCA9685:"); ESP_LOGCONFIG(TAG, " Mode: 0x%02X", this->mode_); - ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_); + if (this->extclk_) { + ESP_LOGCONFIG(TAG, " EXTCLK: enabled"); + } else { + ESP_LOGCONFIG(TAG, " EXTCLK: disabled"); + ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_); + } if (this->is_failed()) { ESP_LOGE(TAG, "Setting up PCA9685 failed!"); } diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 5dd52b5510..8e547d0032 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -37,8 +37,7 @@ class PCA9685Channel : public output::FloatOutput { /// PCA9685 float output component. class PCA9685Output : public Component, public i2c::I2CDevice { public: - PCA9685Output(float frequency, uint8_t mode = PCA9685_MODE_OUTPUT_ONACK | PCA9685_MODE_OUTPUT_TOTEM_POLE) - : frequency_(frequency), mode_(mode) {} + PCA9685Output(uint8_t mode = PCA9685_MODE_OUTPUT_ONACK | PCA9685_MODE_OUTPUT_TOTEM_POLE) : mode_(mode) {} void register_channel(PCA9685Channel *channel); @@ -46,6 +45,8 @@ class PCA9685Output : public Component, public i2c::I2CDevice { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; + void set_extclk(bool extclk) { this->extclk_ = extclk; } + void set_frequency(float frequency) { this->frequency_ = frequency; } protected: friend PCA9685Channel; @@ -58,6 +59,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice { float frequency_; uint8_t mode_; + bool extclk_ = false; uint8_t min_channel_{0xFF}; uint8_t max_channel_{0x00}; diff --git a/esphome/components/pcf85063/__init__.py b/esphome/components/pcf85063/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp new file mode 100644 index 0000000000..c6a8624ca7 --- /dev/null +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -0,0 +1,105 @@ +#include "pcf85063.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://datasheets.maximintegrated.com/en/ds/DS1307.pdf + +namespace esphome { +namespace pcf85063 { + +static const char *const TAG = "pcf85063"; + +void PCF85063Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCF85063..."); + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void PCF85063Component::update() { this->read_time(); } + +void PCF85063Component::dump_config() { + ESP_LOGCONFIG(TAG, "PCF85063:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCF85063 failed!"); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } + +void PCF85063Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (pcf85063_.reg.osc_stop) { + 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)}; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void PCF85063Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + pcf85063_.reg.year = (now.year - 2000) % 10; + pcf85063_.reg.year_10 = (now.year - 2000) / 10 % 10; + pcf85063_.reg.month = now.month % 10; + pcf85063_.reg.month_10 = now.month / 10; + pcf85063_.reg.day = now.day_of_month % 10; + pcf85063_.reg.day_10 = now.day_of_month / 10; + pcf85063_.reg.weekday = now.day_of_week; + pcf85063_.reg.hour = now.hour % 10; + pcf85063_.reg.hour_10 = now.hour / 10; + pcf85063_.reg.minute = now.minute % 10; + pcf85063_.reg.minute_10 = now.minute / 10; + pcf85063_.reg.second = now.second % 10; + pcf85063_.reg.second_10 = now.second / 10; + pcf85063_.reg.osc_stop = false; + + this->write_rtc_(); +} + +bool PCF85063Component::read_rtc_() { + if (!this->read_bytes(0, this->pcf85063_.raw, sizeof(this->pcf85063_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u OSC:%s CLKOUT:%0u", pcf85063_.reg.hour_10, + pcf85063_.reg.hour, pcf85063_.reg.minute_10, pcf85063_.reg.minute, pcf85063_.reg.second_10, + pcf85063_.reg.second, pcf85063_.reg.year_10, pcf85063_.reg.year, pcf85063_.reg.month_10, pcf85063_.reg.month, + pcf85063_.reg.day_10, pcf85063_.reg.day, ONOFF(!pcf85063_.reg.osc_stop), pcf85063_.reg.clkout_control); + + return true; +} + +bool PCF85063Component::write_rtc_() { + if (!this->write_bytes(0, this->pcf85063_.raw, sizeof(this->pcf85063_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u OSC:%s CLKOUT:%0u", pcf85063_.reg.hour_10, + pcf85063_.reg.hour, pcf85063_.reg.minute_10, pcf85063_.reg.minute, pcf85063_.reg.second_10, + pcf85063_.reg.second, pcf85063_.reg.year_10, pcf85063_.reg.year, pcf85063_.reg.month_10, pcf85063_.reg.month, + pcf85063_.reg.day_10, pcf85063_.reg.day, ONOFF(!pcf85063_.reg.osc_stop), pcf85063_.reg.clkout_control); + return true; +} +} // namespace pcf85063 +} // namespace esphome diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h new file mode 100644 index 0000000000..1a3fd704e5 --- /dev/null +++ b/esphome/components/pcf85063/pcf85063.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace pcf85063 { + +class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union PCF85063Reg { + struct { + // Control_1 register + bool cap_12pf : 1; + bool am_pm : 1; + bool correction_int_enable : 1; + bool : 1; + bool soft_reset : 1; + bool stop : 1; + bool : 1; + bool ext_test : 1; + + // Control_2 register + uint8_t clkout_control : 3; + bool timer_flag : 1; + bool halfminute_int : 1; + bool minute_int : 1; + bool alarm_flag : 1; + bool alarm_int : 1; + + // Offset register + uint8_t offset : 7; + bool coarse_mode : 1; + + // nvRAM register + uint8_t nvram : 8; + + // Seconds register + uint8_t second : 4; + uint8_t second_10 : 3; + bool osc_stop : 1; + + // Minutes register + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t : 1; + + // Hours register + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t : 2; + + // Days register + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t : 2; + + // Weekdays register + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + // Months register + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t : 3; + + // Years register + uint8_t year : 4; + uint8_t year_10 : 4; + } reg; + mutable uint8_t raw[sizeof(reg)]; + } pcf85063_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace pcf85063 +} // namespace esphome diff --git a/esphome/components/pcf85063/time.py b/esphome/components/pcf85063/time.py new file mode 100644 index 0000000000..67ec230b5f --- /dev/null +++ b/esphome/components/pcf85063/time.py @@ -0,0 +1,60 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import i2c, time +from esphome.const import CONF_ID + + +CODEOWNERS = ["@brogon"] +DEPENDENCIES = ["i2c"] +pcf85063_ns = cg.esphome_ns.namespace("pcf85063") +PCF85063Component = pcf85063_ns.class_( + "PCF85063Component", time.RealTimeClock, i2c.I2CDevice +) +WriteAction = pcf85063_ns.class_("WriteAction", automation.Action) +ReadAction = pcf85063_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PCF85063Component), + } +).extend(i2c.i2c_device_schema(0x51)) + + +@automation.register_action( + "pcf85063.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PCF85063Component), + } + ), +) +async def pcf85063_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "pcf85063.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(PCF85063Component), + } + ), +) +async def pcf85063_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index fc012aaa39..1b3ddebcc5 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -1,6 +1,10 @@ #include "pid_autotuner.h" #include "esphome/core/log.h" +#ifndef M_PI +#define M_PI 3.1415926535897932384626433 +#endif + namespace esphome { namespace pid { @@ -73,7 +77,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } if (!std::isnan(this->setpoint_) && this->setpoint_ != setpoint) { - ESP_LOGW(TAG, "Setpoint changed during autotune! The result will not be accurate!"); + ESP_LOGW(TAG, "%s: Setpoint changed during autotune! The result will not be accurate!", this->id_.c_str()); } this->setpoint_ = setpoint; @@ -87,7 +91,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce if (!this->frequency_detector_.has_enough_data() || !this->amplitude_detector_.has_enough_data()) { // not enough data for calculation yet - ESP_LOGV(TAG, " Not enough data yet for aututuner"); + ESP_LOGV(TAG, "%s: Not enough data yet for autotuner", this->id_.c_str()); return res; } @@ -97,12 +101,13 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway if (zc_symmetrical) { - ESP_LOGVV(TAG, " ZC is not symmetrical"); + ESP_LOGVV(TAG, "%s: ZC is not symmetrical", this->id_.c_str()); } if (amplitude_convergent) { - ESP_LOGVV(TAG, " Amplitude is not convergent"); + ESP_LOGVV(TAG, "%s: Amplitude is not convergent", this->id_.c_str()); } uint32_t phase = this->relay_function_.phase_count; + ESP_LOGVV(TAG, "%s: >", this->id_.c_str()); ESP_LOGVV(TAG, " Phase %u, enough=%u", phase, enough_data_phase_); if (this->enough_data_phase_ == 0) { @@ -116,7 +121,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } } - ESP_LOGI(TAG, "PID Autotune finished!"); + ESP_LOGI(TAG, "%s: PID Autotune finished!", this->id_.c_str()); float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude(); float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f; @@ -131,12 +136,12 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce return res; } void PIDAutotuner::dump_config() { - ESP_LOGI(TAG, "PID Autotune:"); if (this->state_ == AUTOTUNE_SUCCEEDED) { + ESP_LOGI(TAG, "%s: PID Autotune:", this->id_.c_str()); ESP_LOGI(TAG, " State: Succeeded!"); bool has_issue = false; if (!this->amplitude_detector_.is_amplitude_convergent()) { - ESP_LOGW(TAG, " Could not reliable determine oscillation amplitude, PID parameters may be inaccurate!"); + ESP_LOGW(TAG, " Could not reliably determine oscillation amplitude, PID parameters may be inaccurate!"); ESP_LOGW(TAG, " Please make sure you eliminate all outside influences on the measured temperature."); has_issue = true; } @@ -173,10 +178,12 @@ void PIDAutotuner::dump_config() { print_rule_("Pessen Integral PID", 0.7f, 1.75f, 0.105f); print_rule_("Some Overshoot PID", 0.333f, 0.667f, 0.111f); print_rule_("No Overshoot PID", 0.2f, 0.4f, 0.0625f); + ESP_LOGI(TAG, "%s: Autotune completed", this->id_.c_str()); } if (this->state_ == AUTOTUNE_RUNNING) { - ESP_LOGI(TAG, " Autotune is still running!"); + ESP_LOGD(TAG, "%s: PID Autotune:", this->id_.c_str()); + ESP_LOGD(TAG, " Autotune is still running!"); ESP_LOGD(TAG, " Status: Trying to reach %.2f °C", setpoint_ - relay_function_.current_target_error()); ESP_LOGD(TAG, " Stats so far:"); ESP_LOGD(TAG, " Phases: %u", relay_function_.phase_count); @@ -221,7 +228,6 @@ float PIDAutotuner::RelayFunction::update(float error) { float output = state == RELAY_FUNCTION_POSITIVE ? output_positive : output_negative; if (change) { this->phase_count++; - ESP_LOGV(TAG, "Autotune: Turning output to %.1f%%", output * 100); } return output; @@ -245,10 +251,8 @@ void PIDAutotuner::OscillationFrequencyDetector::update(uint32_t now, float erro if (had_crossing) { // Had crossing above hysteresis threshold, record - ESP_LOGV(TAG, "Autotune: Detected Zero-Cross at %u", now); if (this->last_zerocross != 0) { uint32_t dt = now - this->last_zerocross; - ESP_LOGV(TAG, " dt: %u", dt); this->zerocrossing_intervals.push_back(dt); } this->last_zerocross = now; @@ -297,13 +301,11 @@ void PIDAutotuner::OscillationAmplitudeDetector::update(float error, // The positive error peak must have been in previous segment (180° shifted) // record phase_max this->phase_maxs.push_back(phase_max); - ESP_LOGV(TAG, "Autotune: Phase Max: %f", phase_max); } else if (last_relay_state == RelayFunction::RELAY_FUNCTION_NEGATIVE) { // Transitioned from negative error to positive error. // The negative error peak must have been in previous segment (180° shifted) // record phase_min this->phase_mins.push_back(phase_min); - ESP_LOGV(TAG, "Autotune: Phase Min: %f", phase_min); } // reset phase values for next phase this->phase_min = error; diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h index 88716d2b89..98dc02bcc4 100644 --- a/esphome/components/pid/pid_autotuner.h +++ b/esphome/components/pid/pid_autotuner.h @@ -31,6 +31,8 @@ class PIDAutotuner { void dump_config(); + void set_autotuner_id(std::string id) { this->id_ = std::move(id); } + void set_noiseband(float noiseband) { relay_function_.noiseband = noiseband; // ZC detector uses 1/4 the noiseband of relay function (noise suppression) @@ -106,6 +108,7 @@ class PIDAutotuner { } state_ = AUTOTUNE_RUNNING; float ku_; float pu_; + std::string id_; }; } // namespace pid diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 81c3e1f12e..dab4502d40 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -130,9 +130,6 @@ void PIDClimate::update_pid_() { // keep autotuner instance so that subsequent dump_configs will print the long result message. } else { value = res.output; - if (mode != climate::CLIMATE_MODE_HEAT_COOL) { - ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!"); - } } } } @@ -151,10 +148,24 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { float min_value = this->supports_cool_() ? -1.0f : 0.0f; float max_value = this->supports_heat_() ? 1.0f : 0.0f; this->autotuner_->config(min_value, max_value); + this->autotuner_->set_autotuner_id(this->get_object_id()); + + ESP_LOGI(TAG, + "%s: Autotune has started. This can take a long time depending on the " + "responsiveness of your system. Your system " + "output will be altered to deliberately oscillate above and below the setpoint multiple times. " + "Until your sensor provides a reading, the autotuner may display \'nan\'", + this->get_object_id().c_str()); + this->set_interval("autotune-progress", 10000, [this]() { if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) this->autotuner_->dump_config(); }); + + if (mode != climate::CLIMATE_MODE_HEAT_COOL) { + ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!", + this->get_object_id().c_str()); + } } void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 52b7261f8b..d0c627313c 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -11,43 +11,48 @@ void PulseMeterSensor::setup() { this->isr_pin_ = pin_->to_isr(); this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); - this->pulse_width_us_ = 0; this->last_detected_edge_us_ = 0; - this->last_valid_high_edge_us_ = 0; - this->last_valid_low_edge_us_ = 0; + this->last_valid_edge_us_ = 0; + this->pulse_width_us_ = 0; this->sensor_is_high_ = this->isr_pin_.digital_read(); - this->has_valid_high_edge_ = false; - this->has_valid_low_edge_ = false; + this->has_valid_edge_ = false; + this->pending_state_change_ = NONE; } +// In PULSE mode we set a flag (pending_state_change_) for every interrupt +// that constitutes a state change. In the loop() method we check if a time +// interval greater than the internal_filter time has passed without any +// interrupts. void PulseMeterSensor::loop() { - // Get a local copy of the volatile sensor values, to make sure they are not - // modified by the ISR. This could cause overflow in the following arithmetic - const uint32_t last_valid_high_edge_us = this->last_valid_high_edge_us_; - const bool has_valid_high_edge = this->has_valid_high_edge_; + // Get a snapshot of the needed volatile sensor values, to make sure they are not + // modified by the ISR while we are in the loop() method. If they are changed + // after we the variable "now" has been set, overflow will occur in the + // subsequent arithmetic + const bool has_valid_edge = this->has_valid_edge_; + const uint32_t last_detected_edge_us = this->last_detected_edge_us_; + const uint32_t last_valid_edge_us = this->last_valid_edge_us_; + // Get the current time after the snapshot of saved times const uint32_t now = micros(); - // If we've exceeded our timeout interval without receiving any pulses, assume - // 0 pulses/min until we get at least two valid pulses. - const uint32_t time_since_valid_edge_us = now - last_valid_high_edge_us; - if ((has_valid_high_edge) && (time_since_valid_edge_us > this->timeout_us_)) { + this->handle_state_change_(now, last_detected_edge_us, last_valid_edge_us, has_valid_edge); + + // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until + // we get at least two valid pulses. + const uint32_t time_since_valid_edge_us = now - last_detected_edge_us; + if ((has_valid_edge) && (time_since_valid_edge_us > this->timeout_us_)) { ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); + this->last_valid_edge_us_ = 0; this->pulse_width_us_ = 0; + this->has_valid_edge_ = false; this->last_detected_edge_us_ = 0; - this->last_valid_high_edge_us_ = 0; - this->last_valid_low_edge_us_ = 0; - this->has_detected_edge_ = false; - this->has_valid_high_edge_ = false; - this->has_valid_low_edge_ = false; } // We quantize our pulse widths to 1 ms to avoid unnecessary jitter const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; if (this->pulse_width_dedupe_.next(pulse_width_ms)) { if (pulse_width_ms == 0) { - // Treat 0 pulse width as 0 pulses/min (normally because we've not - // detected any pulses for a while) + // Treat 0 pulse width as 0 pulses/min (normally because we've not detected any pulses for a while) this->publish_state(0); } else { // Calculate pulses/min from the pulse width in ms @@ -77,58 +82,95 @@ void PulseMeterSensor::dump_config() { } void IRAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { - // This is an interrupt handler - we can't call any virtual method from this - // method - - // Get the current time before we do anything else so the measurements are - // consistent + // This is an interrupt handler - we can't call any virtual method from this method + // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); + const bool pin_val = sensor->isr_pin_.digital_read(); - // We only look at rising edges in EDGE mode, and all edges in PULSE mode if (sensor->filter_mode_ == FILTER_EDGE) { - if (sensor->isr_pin_.digital_read()) { - sensor->last_detected_edge_us_ = now; - } - } - - // Check to see if we should filter this edge out - if (sensor->filter_mode_ == FILTER_EDGE) { - if ((sensor->last_detected_edge_us_ - sensor->last_valid_high_edge_us_) >= sensor->filter_us_) { - // Don't measure the first valid pulse (we need at least two pulses to - // measure the width) - if (sensor->has_valid_high_edge_) { - sensor->pulse_width_us_ = (sensor->last_detected_edge_us_ - sensor->last_valid_high_edge_us_); - } - sensor->total_pulses_++; - sensor->last_valid_high_edge_us_ = sensor->last_detected_edge_us_; - sensor->has_valid_high_edge_ = true; - } - } else { - // Filter Mode is PULSE - bool pin_val = sensor->isr_pin_.digital_read(); - // Ignore false edges that may be caused by bouncing and exit the ISR ASAP - if (pin_val == sensor->sensor_is_high_) { + // We only look at rising edges + if (!pin_val) { return; } - // Make sure the signal has been stable long enough - if (sensor->has_detected_edge_ && (now - sensor->last_detected_edge_us_ >= sensor->filter_us_)) { - if (pin_val) { - sensor->has_valid_high_edge_ = true; - sensor->last_valid_high_edge_us_ = sensor->last_detected_edge_us_; - sensor->sensor_is_high_ = true; - } else { - // Count pulses when a sufficiently long high pulse is concluded. - sensor->total_pulses_++; - if (sensor->has_valid_low_edge_) { - sensor->pulse_width_us_ = sensor->last_detected_edge_us_ - sensor->last_valid_low_edge_us_; - } - sensor->has_valid_low_edge_ = true; - sensor->last_valid_low_edge_us_ = sensor->last_detected_edge_us_; - sensor->sensor_is_high_ = false; + // Check to see if we should filter this edge out + if ((now - sensor->last_detected_edge_us_) >= sensor->filter_us_) { + // Don't measure the first valid pulse (we need at least two pulses to measure the width) + if (sensor->has_valid_edge_) { + sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_); } + sensor->total_pulses_++; + sensor->last_valid_edge_us_ = now; + sensor->has_valid_edge_ = true; } - sensor->has_detected_edge_ = true; sensor->last_detected_edge_us_ = now; + } else { + // Filter Mode is PULSE + const uint32_t delta_t_us = now - sensor->last_detected_edge_us_; + // We need to check if we have missed to handle a state change in the + // loop() function. This can happen when the filter_us value is less than + // the loop() interval, which is ~50-60ms + // The section below is essentially a modified repeat of the + // handle_state_change method. Ideally i would refactor and call the + // method here as well. However functions called in ISRs need to meet + // strict criteria and I don't think the methos would meet them. + if (sensor->pending_state_change_ != NONE && (delta_t_us > sensor->filter_us_)) { + // We have missed to handle a state change in the loop function. + sensor->sensor_is_high_ = sensor->pending_state_change_ == TO_HIGH; + if (sensor->sensor_is_high_) { + // We need to handle a pulse that would have been missed by the loop function + sensor->total_pulses_++; + if (sensor->has_valid_edge_) { + sensor->pulse_width_us_ = sensor->last_detected_edge_us_ - sensor->last_valid_edge_us_; + sensor->has_valid_edge_ = true; + sensor->last_valid_edge_us_ = sensor->last_detected_edge_us_; + } + } + } // End of checking for and handling of change in state + + // Ignore false edges that may be caused by bouncing and exit the ISR ASAP + if (pin_val == sensor->sensor_is_high_) { + sensor->pending_state_change_ = NONE; + return; + } + sensor->pending_state_change_ = pin_val ? TO_HIGH : TO_LOW; + sensor->last_detected_edge_us_ = now; + } +} + +void PulseMeterSensor::handle_state_change_(uint32_t now, uint32_t last_detected_edge_us, uint32_t last_valid_edge_us, + bool has_valid_edge) { + if (this->pending_state_change_ == NONE) { + return; + } + + const bool pin_val = this->isr_pin_.digital_read(); + if (pin_val == this->sensor_is_high_) { + // Most likely caused by high frequency bouncing. Theoretically we should + // expect interrupts of alternating state. Here we are registering an + // interrupt with no change in state. Another interrupt will likely trigger + // just after this one and have an alternate state. + this->pending_state_change_ = NONE; + return; + } + + if ((now - last_detected_edge_us) > this->filter_us_) { + this->sensor_is_high_ = pin_val; + ESP_LOGVV(TAG, "State is now %s", pin_val ? "high" : "low"); + + // Increment with valid rising edges only + if (pin_val) { + this->total_pulses_++; + ESP_LOGVV(TAG, "Incremented pulses to %u", this->total_pulses_); + + if (has_valid_edge) { + this->pulse_width_us_ = last_detected_edge_us - last_valid_edge_us; + ESP_LOGVV(TAG, "Set pulse width to %u", this->pulse_width_us_); + } + this->has_valid_edge_ = true; + this->last_valid_edge_us_ = last_detected_edge_us; + ESP_LOGVV(TAG, "last_valid_edge_us_ is now %u", this->last_valid_edge_us_); + } + this->pending_state_change_ = NONE; } } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index ed4fb2a1f4..47af6e2398 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -29,7 +29,11 @@ class PulseMeterSensor : public sensor::Sensor, public Component { void dump_config() override; protected: + enum StateChange { TO_LOW = 0, TO_HIGH, NONE }; + static void gpio_intr(PulseMeterSensor *sensor); + void handle_state_change_(uint32_t now, uint32_t last_detected_edge_us, uint32_t last_valid_edge_us, + bool has_valid_edge); InternalGPIOPin *pin_{nullptr}; ISRInternalGPIOPin isr_pin_; @@ -42,14 +46,12 @@ class PulseMeterSensor : public sensor::Sensor, public Component { Deduplicator total_dedupe_; volatile uint32_t last_detected_edge_us_ = 0; - volatile uint32_t last_valid_high_edge_us_ = 0; - volatile uint32_t last_valid_low_edge_us_ = 0; + volatile uint32_t last_valid_edge_us_ = 0; volatile uint32_t pulse_width_us_ = 0; volatile uint32_t total_pulses_ = 0; volatile bool sensor_is_high_ = false; - volatile bool has_detected_edge_ = false; - volatile bool has_valid_high_edge_ = false; - volatile bool has_valid_low_edge_ = false; + volatile bool has_valid_edge_ = false; + volatile StateChange pending_state_change_{NONE}; }; } // namespace pulse_meter diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index b979a050db..c3149ce430 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -237,6 +237,107 @@ async def build_dumpers(config): return dumpers +# CanalSat +( + CanalSatData, + CanalSatBinarySensor, + CanalSatTrigger, + CanalSatAction, + CanalSatDumper, +) = declare_protocol("CanalSat") +CANALSAT_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint8_t, + cv.Optional(CONF_ADDRESS, default=0): cv.hex_uint8_t, + cv.Required(CONF_COMMAND): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("canalsat", CanalSatBinarySensor, CANALSAT_SCHEMA) +def canalsat_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CanalSatData, + ("device", config[CONF_DEVICE]), + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("canalsat", CanalSatTrigger, CanalSatData) +def canalsat_trigger(var, config): + pass + + +@register_dumper("canalsat", CanalSatDumper) +def canalsat_dumper(var, config): + pass + + +@register_action("canalsat", CanalSatAction, CANALSAT_SCHEMA) +async def canalsat_action(var, config, args): + template_ = await cg.templatable(config[CONF_DEVICE], args, cg.uint8) + cg.add(var.set_device(template_)) + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + +( + CanalSatLDData, + CanalSatLDBinarySensor, + CanalSatLDTrigger, + CanalSatLDAction, + CanalSatLDDumper, +) = declare_protocol("CanalSatLD") +CANALSATLD_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint8_t, + cv.Optional(CONF_ADDRESS, default=0): cv.hex_uint8_t, + cv.Required(CONF_COMMAND): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSAT_SCHEMA) +def canalsatld_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CanalSatLDData, + ("device", config[CONF_DEVICE]), + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("canalsatld", CanalSatLDTrigger, CanalSatLDData) +def canalsatld_trigger(var, config): + pass + + +@register_dumper("canalsatld", CanalSatLDDumper) +def canalsatld_dumper(var, config): + pass + + +@register_action("canalsatld", CanalSatLDAction, CANALSATLD_SCHEMA) +async def canalsatld_action(var, config, args): + template_ = await cg.templatable(config[CONF_DEVICE], args, cg.uint8) + cg.add(var.set_device(template_)) + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # Coolix ( CoolixData, diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp new file mode 100644 index 0000000000..1ea47750fd --- /dev/null +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -0,0 +1,108 @@ +#include "canalsat_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const CANALSAT_TAG = "remote.canalsat"; +static const char *const CANALSATLD_TAG = "remote.canalsatld"; + +static const uint16_t CANALSAT_FREQ = 55500; +static const uint16_t CANALSATLD_FREQ = 56000; +static const uint16_t CANALSAT_UNIT = 250; +static const uint16_t CANALSATLD_UNIT = 320; + +CanalSatProtocol::CanalSatProtocol() { + this->frequency_ = CANALSAT_FREQ; + this->unit_ = CANALSAT_UNIT; + this->tag_ = CANALSAT_TAG; +} + +CanalSatLDProtocol::CanalSatLDProtocol() { + this->frequency_ = CANALSATLD_FREQ; + this->unit_ = CANALSATLD_UNIT; + this->tag_ = CANALSATLD_TAG; +} + +void CanalSatBaseProtocol::encode(RemoteTransmitData *dst, const CanalSatData &data) { + dst->reserve(48); + dst->set_carrier_frequency(this->frequency_); + + uint32_t raw{ + static_cast((1 << 23) | (data.device << 16) | (data.address << 10) | (0 << 9) | (data.command << 1))}; + bool was_high{true}; + + for (uint32_t mask = 0x800000; mask; mask >>= 1) { + if (raw & mask) { + if (was_high) { + dst->mark(this->unit_); + } + was_high = true; + if (raw & mask >> 1) { + dst->space(this->unit_); + } else { + dst->space(this->unit_ * 2); + } + } else { + if (!was_high) { + dst->space(this->unit_); + } + was_high = false; + if (raw & mask >> 1) { + dst->mark(this->unit_ * 2); + } else { + dst->mark(this->unit_); + } + } + } +} + +optional CanalSatBaseProtocol::decode(RemoteReceiveData src) { + CanalSatData data{ + .device = 0, + .address = 0, + .repeat = 0, + .command = 0, + }; + + // Check if initial mark and spaces match + if (!src.peek_mark(this->unit_) || !(src.peek_space(this->unit_, 1) || src.peek_space(this->unit_ * 2, 1))) { + return {}; + } + + uint8_t bit{1}; + uint8_t offset{1}; + uint32_t buffer{0}; + + while (offset < 24) { + buffer = buffer | (bit << (24 - offset++)); + src.advance(); + if (src.peek_mark(this->unit_) || src.peek_space(this->unit_)) { + src.advance(); + } else if (src.peek_mark(this->unit_ * 2) || src.peek_space(this->unit_ * 2)) { + bit = !bit; + } else if (offset != 24 && bit != 1) { // If last bit is high, final space is indistinguishable + return {}; + } + } + + data.device = (0xFF0000 & buffer) >> 16; + data.address = (0x00FF00 & buffer) >> 10; + data.repeat = (0x00FF00 & buffer) >> 9; + data.command = (0x0000FF & buffer) >> 1; + + return data; +} + +void CanalSatBaseProtocol::dump(const CanalSatData &data) { + if (this->tag_ == CANALSATLD_TAG) { + ESP_LOGD(this->tag_, "Received CanalSatLD: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, + data.address, data.command, data.repeat); + } else { + ESP_LOGD(this->tag_, "Received CanalSat: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device, + data.address, data.command, data.repeat); + } +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/canalsat_protocol.h b/esphome/components/remote_base/canalsat_protocol.h new file mode 100644 index 0000000000..180989ef99 --- /dev/null +++ b/esphome/components/remote_base/canalsat_protocol.h @@ -0,0 +1,78 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct CanalSatData { + uint8_t device : 7; + uint8_t address : 6; + uint8_t repeat : 1; + uint8_t command : 7; + + bool operator==(const CanalSatData &rhs) const { + return device == rhs.device && address == rhs.address && command == rhs.command; + } +}; + +struct CanalSatLDData : public CanalSatData {}; + +class CanalSatBaseProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const CanalSatData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const CanalSatData &data) override; + + protected: + uint16_t frequency_; + uint16_t unit_; + const char *tag_; +}; + +class CanalSatProtocol : public CanalSatBaseProtocol { + public: + CanalSatProtocol(); +}; + +class CanalSatLDProtocol : public CanalSatBaseProtocol { + public: + CanalSatLDProtocol(); +}; + +DECLARE_REMOTE_PROTOCOL(CanalSat) + +template class CanalSatAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, device) + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) { + CanalSatData data{}; + data.device = this->device_.value(x...); + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + CanalSatProtocol().encode(dst, data); + } +}; + +DECLARE_REMOTE_PROTOCOL(CanalSatLD) + +template class CanalSatLDAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, device) + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) { + CanalSatData data{}; + data.device = this->device_.value(x...); + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + CanalSatLDProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp index 664a3734f7..3c5591885e 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace esphome { namespace rp2040_pwm { @@ -23,8 +24,14 @@ void RP2040PWM::setup() { void RP2040PWM::setup_pwm_() { pwm_config config = pwm_get_default_config(); - pwm_config_set_clkdiv(&config, clock_get_hz(clk_sys) / (255.0f * this->frequency_)); - pwm_config_set_wrap(&config, 254); + + uint32_t clock = clock_get_hz(clk_sys); + float divider = ceil(clock / (4096 * this->frequency_)) / 16.0f; + uint16_t wrap = clock / divider / this->frequency_ - 1; + this->wrap_ = wrap; + + pwm_config_set_clkdiv(&config, divider); + pwm_config_set_wrap(&config, wrap); pwm_init(pwm_gpio_to_slice_num(this->pin_->get_pin()), &config, true); } @@ -48,7 +55,7 @@ void HOT RP2040PWM::write_state(float state) { } gpio_set_function(this->pin_->get_pin(), GPIO_FUNC_PWM); - pwm_set_gpio_level(this->pin_->get_pin(), state * 255.0f); + pwm_set_gpio_level(this->pin_->get_pin(), state * this->wrap_); } } // namespace rp2040_pwm diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h index 9c88826ae9..e499e72b06 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.h +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -34,6 +34,7 @@ class RP2040PWM : public output::FloatOutput, public Component { InternalGPIOPin *pin_; float frequency_{1000.0}; + uint16_t wrap_{65535}; /// Cache last output level for dynamic frequency updating float last_output_{0.0}; bool frequency_changed_{false}; diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h new file mode 100644 index 0000000000..37b3bc1674 --- /dev/null +++ b/esphome/components/scd30/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "scd30.h" + +namespace esphome { +namespace scd30 { + +template class ForceRecalibrationWithReference : public Action, public Parented { + public: + void play(Ts... x) override { + if (this->value_.has_value()) { + this->parent_->force_recalibration_with_reference(this->value_.value(x...)); + } + } + + protected: + TEMPLATABLE_VALUE(uint16_t, value) +}; + +} // namespace scd30 +} // namespace esphome diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 103b7a255d..c25ed107b7 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -202,5 +202,27 @@ bool SCD30Component::is_data_ready_() { return is_data_ready == 1; } +bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) { + ESP_LOGD(TAG, "Performing CO2 force recalibration with reference %dppm.", co2_reference); + if (this->write_command(SCD30_CMD_FORCED_CALIBRATION, co2_reference)) { + ESP_LOGD(TAG, "Force recalibration complete."); + return true; + } else { + ESP_LOGE(TAG, "Failed to force recalibration with reference."); + this->error_code_ = FORCE_RECALIBRATION_FAILED; + this->status_set_warning(); + return false; + } +} + +uint16_t SCD30Component::get_forced_calibration_reference() { + uint16_t forced_calibration_reference; + // Get current CO2 calibration + if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) { + ESP_LOGE(TAG, "Unable to read forced calibration reference."); + } + return forced_calibration_reference; +} + } // namespace scd30 } // namespace esphome diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index c434bf0dea..4a4ca832bf 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -20,6 +20,8 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe } void set_temperature_offset(float offset) { temperature_offset_ = offset; } void set_update_interval(uint16_t interval) { update_interval_ = interval; } + bool force_recalibration_with_reference(uint16_t co2_reference); + uint16_t get_forced_calibration_reference(); void setup() override; void update(); @@ -33,6 +35,7 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe COMMUNICATION_FAILED, FIRMWARE_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, + FORCE_RECALIBRATION_FAILED, UNKNOWN } error_code_{UNKNOWN}; bool enable_asc_{true}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 3cfd861a63..ffbf90338f 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,4 +1,4 @@ -from esphome import core +from esphome import automation, core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor @@ -9,6 +9,7 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_CO2, CONF_UPDATE_INTERVAL, + CONF_VALUE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -26,6 +27,11 @@ SCD30Component = scd30_ns.class_( "SCD30Component", cg.Component, sensirion_common.SensirionI2CDevice ) +# Actions +ForceRecalibrationWithReference = scd30_ns.class_( + "ForceRecalibrationWithReference", automation.Action +) + CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" @@ -106,3 +112,26 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + + +@automation.register_action( + "scd30.force_recalibration_with_reference", + ForceRecalibrationWithReference, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(SCD30Component), + cv.Required(CONF_VALUE): cv.templatable( + cv.int_range(min=400, max=2000, max_included=True) + ), + }, + key=CONF_VALUE, + ), +) +async def scd30_force_recalibration_with_reference_to_code( + config, action_id, template_arg, args +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) + cg.add(var.set_value(template_)) + return var diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 681324fa18..117b92b901 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -149,9 +149,9 @@ void SCD4XComponent::update() { } if (this->ambient_pressure_source_ != nullptr) { - float pressure = this->ambient_pressure_source_->state / 1000.0f; + float pressure = this->ambient_pressure_source_->state; if (!std::isnan(pressure)) { - set_ambient_pressure_compensation(this->ambient_pressure_source_->state / 1000.0f); + set_ambient_pressure_compensation(pressure); } } @@ -254,12 +254,15 @@ bool SCD4XComponent::factory_reset() { return true; } -// Note pressure in bar here. Convert to hPa -void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { +void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) { ambient_pressure_compensation_ = true; - uint16_t new_ambient_pressure = (uint16_t)(pressure_in_bar * 1000); - // remove millibar from comparison to avoid frequent updates +/- 10 millibar doesn't matter - if (initialized_ && (new_ambient_pressure / 10 != ambient_pressure_ / 10)) { + uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa; + if (!initialized_) { + ambient_pressure_ = new_ambient_pressure; + return; + } + // Only send pressure value if it has changed since last update + if (new_ambient_pressure != ambient_pressure_) { update_ambient_pressure_compensation_(new_ambient_pressure); ambient_pressure_ = new_ambient_pressure; } else { diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 23c3766e60..22055e78d0 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -26,7 +26,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } - void set_ambient_pressure_compensation(float pressure_in_bar); + void set_ambient_pressure_compensation(float pressure_in_hpa); void set_ambient_pressure_source(sensor::Sensor *pressure) { ambient_pressure_source_ = pressure; } void set_temperature_offset(float offset) { temperature_offset_ = offset; }; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 2859dc6e5a..7842cef4de 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -55,6 +55,7 @@ from esphome.const import ( DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, @@ -103,6 +104,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_REACTIVE_POWER, @@ -115,8 +117,8 @@ DEVICE_CLASSES = [ DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLUME, DEVICE_CLASS_WATER, - DEVICE_CLASS_WIND_SPEED, DEVICE_CLASS_WEIGHT, + DEVICE_CLASS_WIND_SPEED, ] sensor_ns = cg.esphome_ns.namespace("sensor") diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 958230cb3c..bba69ed0bb 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -31,6 +31,13 @@ namespace sensor { } \ } +#define SUB_SENSOR(name) \ + protected: \ + sensor::Sensor *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_ = sensor; } + /** * Sensor state classes */ diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 1f6c5006a5..3d24f6c409 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -6,8 +6,7 @@ from esphome.const import ( CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, ICON_RADIATOR, - DEVICE_CLASS_NITROUS_OXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_AQI, STATE_CLASS_MEASUREMENT, ) @@ -67,13 +66,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOC): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend(GAS_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_NITROUS_OXIDE, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend(GAS_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index 9050a872b1..20e0e8156b 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -22,6 +22,7 @@ from esphome.const import ( UNIT_WATT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, ) from esphome.core import HexInt, CORE @@ -169,7 +170,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_CURRENT, accuracy_decimals=2, ), # Change the default gamma_correct setting. diff --git a/esphome/components/sigma_delta_output/__init__.py b/esphome/components/sigma_delta_output/__init__.py new file mode 100644 index 0000000000..3356e61bb2 --- /dev/null +++ b/esphome/components/sigma_delta_output/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/sigma_delta_output/output.py b/esphome/components/sigma_delta_output/output.py new file mode 100644 index 0000000000..49ac9e685a --- /dev/null +++ b/esphome/components/sigma_delta_output/output.py @@ -0,0 +1,66 @@ +from esphome import automation, pins +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_ID, + CONF_PIN, + CONF_TURN_ON_ACTION, + CONF_TURN_OFF_ACTION, +) + +DEPENDENCIES = [] + + +sigma_delta_output_ns = cg.esphome_ns.namespace("sigma_delta_output") +SigmaDeltaOutput = sigma_delta_output_ns.class_( + "SigmaDeltaOutput", output.FloatOutput, cg.PollingComponent +) + +CONF_STATE_CHANGE_ACTION = "state_change_action" + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend(cv.polling_component_schema("60s")).extend( + { + cv.Required(CONF_ID): cv.declare_id(SigmaDeltaOutput), + cv.Optional(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation( + single=True + ), + cv.Inclusive( + CONF_TURN_ON_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + cv.Inclusive( + CONF_TURN_OFF_ACTION, + "on_off", + f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined", + ): automation.validate_automation(single=True), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + if CONF_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + + if CONF_STATE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_state_change_trigger(), + [(bool, "state")], + config[CONF_STATE_CHANGE_ACTION], + ) + if CONF_TURN_ON_ACTION in config: + await automation.build_automation( + var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] + ) + await automation.build_automation( + var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] + ) diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h new file mode 100644 index 0000000000..5a5acd2dfb --- /dev/null +++ b/esphome/components/sigma_delta_output/sigma_delta_output.h @@ -0,0 +1,66 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sigma_delta_output { +class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { + public: + Trigger<> *get_turn_on_trigger() { + if (!this->turn_on_trigger_) + this->turn_on_trigger_ = make_unique>(); + return this->turn_on_trigger_.get(); + } + Trigger<> *get_turn_off_trigger() { + if (!this->turn_off_trigger_) + this->turn_off_trigger_ = make_unique>(); + return this->turn_off_trigger_.get(); + } + + Trigger *get_state_change_trigger() { + if (!this->state_change_trigger_) + this->state_change_trigger_ = make_unique>(); + return this->state_change_trigger_.get(); + } + + void set_pin(GPIOPin *pin) { this->pin_ = pin; }; + void write_state(float state) override { this->state_ = state; } + void update() override { + this->accum_ += this->state_; + const bool next_value = this->accum_ > 0; + + if (next_value) { + this->accum_ -= 1.; + } + + if (next_value != this->value_) { + this->value_ = next_value; + if (this->pin_) { + this->pin_->digital_write(next_value); + } + + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(next_value); + } + + if (next_value && this->turn_on_trigger_) { + this->turn_on_trigger_->trigger(); + } else if (!next_value && this->turn_off_trigger_) { + this->turn_off_trigger_->trigger(); + } + } + } + + protected: + GPIOPin *pin_{nullptr}; + + std::unique_ptr> turn_on_trigger_{nullptr}; + std::unique_ptr> turn_off_trigger_{nullptr}; + std::unique_ptr> state_change_trigger_{nullptr}; + + float accum_{0}; + float state_{0.}; + bool value_{false}; +}; +} // namespace sigma_delta_output +} // namespace esphome diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 7015933e73..4f7aa228e9 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -156,6 +156,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_SEND_USSD1: this->send_cmd_("AT+CUSD=1, \"" + this->ussd_ + "\""); this->state_ = STATE_SEND_USSD2; + this->expect_ack_ = true; break; case STATE_SEND_USSD2: ESP_LOGD(TAG, "SendUssd2: '%s'", message.c_str()); diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 81c721a866..d6b2cdfe12 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -70,7 +70,7 @@ void SlowPWMOutput::dump_config() { void SlowPWMOutput::write_state(float state) { this->state_ = state; if (this->restart_cycle_on_state_change_) - this->period_start_time_ = millis(); + this->restart_cycle(); } } // namespace slow_pwm diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index be45736864..3e5a3e2a40 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -14,6 +14,7 @@ class SlowPWMOutput : public output::FloatOutput, public Component { void set_restart_cycle_on_state_change(bool restart_cycle_on_state_change) { restart_cycle_on_state_change_ = restart_cycle_on_state_change; } + void restart_cycle() { this->period_start_time_ = millis(); } /// Initialize pin void setup() override; diff --git a/esphome/components/sm10bit_base/__init__.py b/esphome/components/sm10bit_base/__init__.py new file mode 100644 index 0000000000..8722bd35a9 --- /dev/null +++ b/esphome/components/sm10bit_base/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, +) + +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels" +CONF_MAX_POWER_WHITE_CHANNELS = "max_power_white_channels" + +sm10bit_base_ns = cg.esphome_ns.namespace("sm10bit_base") +Sm10BitBase = sm10bit_base_ns.class_("Sm10BitBase", cg.Component) + +SM10BIT_BASE_CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_MAX_POWER_COLOR_CHANNELS, default=2): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_MAX_POWER_WHITE_CHANNELS, default=4): cv.int_range( + min=0, max=15 + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def register_sm10bit_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS])) + cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS])) + + return var diff --git a/esphome/components/sm10bit_base/sm10bit_base.cpp b/esphome/components/sm10bit_base/sm10bit_base.cpp new file mode 100644 index 0000000000..9c7abb48e2 --- /dev/null +++ b/esphome/components/sm10bit_base/sm10bit_base.cpp @@ -0,0 +1,112 @@ +#include "sm10bit_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm10bit_base { + +static const char *const TAG = "sm10bit_base"; + +static const uint8_t SM10BIT_ADDR_STANDBY = 0x0; +static const uint8_t SM10BIT_ADDR_START_3CH = 0x8; +static const uint8_t SM10BIT_ADDR_START_2CH = 0x10; +static const uint8_t SM10BIT_ADDR_START_5CH = 0x18; + +// Power current values +// HEX | Binary | RGB level | White level | Config value +// 0x0 | 0000 | RGB 10mA | CW 5mA | 0 +// 0x1 | 0001 | RGB 20mA | CW 10mA | 1 +// 0x2 | 0010 | RGB 30mA | CW 15mA | 2 - Default spec color value +// 0x3 | 0011 | RGB 40mA | CW 20mA | 3 +// 0x4 | 0100 | RGB 50mA | CW 25mA | 4 - Default spec white value +// 0x5 | 0101 | RGB 60mA | CW 30mA | 5 +// 0x6 | 0110 | RGB 70mA | CW 35mA | 6 +// 0x7 | 0111 | RGB 80mA | CW 40mA | 7 +// 0x8 | 1000 | RGB 90mA | CW 45mA | 8 +// 0x9 | 1001 | RGB 100mA | CW 50mA | 9 +// 0xA | 1010 | RGB 110mA | CW 55mA | 10 +// 0xB | 1011 | RGB 120mA | CW 60mA | 11 +// 0xC | 1100 | RGB 130mA | CW 65mA | 12 +// 0xD | 1101 | RGB 140mA | CW 70mA | 13 +// 0xE | 1110 | RGB 150mA | CW 75mA | 14 +// 0xF | 1111 | RGB 160mA | CW 80mA | 15 + +void Sm10BitBase::loop() { + if (!this->update_) + return; + + uint8_t data[12]; + if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Off / Sleep + data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY; + for (int i = 1; i < 12; i++) + data[i] = 0; + this->write_buffer_(data, 12); + } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && + (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { + // Only data on white channels + data[0] = this->model_id_ + SM10BIT_ADDR_START_2CH; + data[1] = 0 << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] >> 0x8; + data[i + 1] = this->pwm_amounts_[j] & 0xFF; + } + this->write_buffer_(data, 12); + } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) && + this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { + // Only data on RGB channels + data[0] = this->model_id_ + SM10BIT_ADDR_START_3CH; + data[1] = this->max_power_color_channels_ << 4 | 0; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] >> 0x8; + data[i + 1] = this->pwm_amounts_[j] & 0xFF; + } + this->write_buffer_(data, 12); + } else { + // All channels + data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH; + data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_; + for (int i = 2, j = 0; i < 12; i += 2, j++) { + data[i] = this->pwm_amounts_[j] >> 0x8; + data[i + 1] = this->pwm_amounts_[j] & 0xFF; + } + this->write_buffer_(data, 12); + } + + this->update_ = false; +} + +void Sm10BitBase::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} +void Sm10BitBase::write_bit_(bool value) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); +} + +void Sm10BitBase::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(true); + this->clock_pin_->digital_write(true); +} + +void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) { + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + this->data_pin_->digital_write(true); +} + +} // namespace sm10bit_base +} // namespace esphome diff --git a/esphome/components/sm10bit_base/sm10bit_base.h b/esphome/components/sm10bit_base/sm10bit_base.h new file mode 100644 index 0000000000..c8e92e352f --- /dev/null +++ b/esphome/components/sm10bit_base/sm10bit_base.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" +#include + +namespace esphome { +namespace sm10bit_base { + +class Sm10BitBase : public Component { + public: + class Channel; + + void set_model(uint8_t model_id) { model_id_ = model_id; } + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_max_power_color_channels(uint8_t max_power_color_channels) { + max_power_color_channels_ = max_power_color_channels; + } + void set_max_power_white_channels(uint8_t max_power_white_channels) { + max_power_white_channels_ = max_power_white_channels; + } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void setup() override; + void dump_config() override; + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(Sm10BitBase *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0x3FF); + this->parent_->set_channel_value_(this->channel_, amount); + } + + Sm10BitBase *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint16_t value); + void write_bit_(bool value); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t model_id_; + uint8_t max_power_color_channels_{2}; + uint8_t max_power_white_channels_{4}; + uint8_t update_channel_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace sm10bit_base +} // namespace esphome diff --git a/esphome/components/sm2235/__init__.py b/esphome/components/sm2235/__init__.py new file mode 100644 index 0000000000..ae6cb336ad --- /dev/null +++ b/esphome/components/sm2235/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sm10bit_base + +AUTO_LOAD = ["sm10bit_base", "output"] +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +sm2235_ns = cg.esphome_ns.namespace("sm2235") + +SM2235 = sm2235_ns.class_("SM2235", sm10bit_base.Sm10BitBase) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM2235), + } +).extend(sm10bit_base.SM10BIT_BASE_CONFIG_SCHEMA) + + +async def to_code(config): + var = await sm10bit_base.register_sm10bit_base(config) + cg.add(var.set_model(0xC0)) diff --git a/esphome/components/sm2235/output.py b/esphome/components/sm2235/output.py new file mode 100644 index 0000000000..c4f63b451a --- /dev/null +++ b/esphome/components/sm2235/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM2235 + +DEPENDENCIES = ["sm2235"] +CODEOWNERS = ["@Cossid"] + +Channel = SM2235.class_("Channel", output.FloatOutput) + +CONF_SM2235_ID = "sm2235_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM2235_ID): cv.use_id(SM2235), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_SM2235_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm2235/sm2235.cpp b/esphome/components/sm2235/sm2235.cpp new file mode 100644 index 0000000000..f953d41957 --- /dev/null +++ b/esphome/components/sm2235/sm2235.cpp @@ -0,0 +1,27 @@ +#include "sm2235.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm2235 { + +static const char *const TAG = "sm2235"; + +void SM2235::setup() { + ESP_LOGCONFIG(TAG, "Setting up sm2235 Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->pwm_amounts_.resize(5, 0); +} + +void SM2235::dump_config() { + ESP_LOGCONFIG(TAG, "sm2235:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Color Channels Max Power: %u", this->max_power_color_channels_); + ESP_LOGCONFIG(TAG, " White Channels Max Power: %u", this->max_power_white_channels_); +} + +} // namespace sm2235 +} // namespace esphome diff --git a/esphome/components/sm2235/sm2235.h b/esphome/components/sm2235/sm2235.h new file mode 100644 index 0000000000..56d1782055 --- /dev/null +++ b/esphome/components/sm2235/sm2235.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sm10bit_base/sm10bit_base.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sm2235 { + +class SM2235 : public sm10bit_base::Sm10BitBase { + public: + SM2235() = default; + + void setup() override; + void dump_config() override; +}; + +} // namespace sm2235 +} // namespace esphome diff --git a/esphome/components/sm2335/__init__.py b/esphome/components/sm2335/__init__.py new file mode 100644 index 0000000000..6d6e0e311c --- /dev/null +++ b/esphome/components/sm2335/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sm10bit_base + +AUTO_LOAD = ["sm10bit_base", "output"] +CODEOWNERS = ["@Cossid"] +MULTI_CONF = True + +sm2335_ns = cg.esphome_ns.namespace("sm2335") + +SM2335 = sm2335_ns.class_("SM2335", sm10bit_base.Sm10BitBase) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SM2335), + } +).extend(sm10bit_base.SM10BIT_BASE_CONFIG_SCHEMA) + + +async def to_code(config): + var = await sm10bit_base.register_sm10bit_base(config) + cg.add(var.set_model(0xC0)) diff --git a/esphome/components/sm2335/output.py b/esphome/components/sm2335/output.py new file mode 100644 index 0000000000..52b6321db1 --- /dev/null +++ b/esphome/components/sm2335/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM2335 + +DEPENDENCIES = ["sm2335"] +CODEOWNERS = ["@Cossid"] + +Channel = SM2335.class_("Channel", output.FloatOutput) + +CONF_SM2335_ID = "sm2335_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_SM2335_ID): cv.use_id(SM2335), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_SM2335_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm2335/sm2335.cpp b/esphome/components/sm2335/sm2335.cpp new file mode 100644 index 0000000000..b6c482b5bb --- /dev/null +++ b/esphome/components/sm2335/sm2335.cpp @@ -0,0 +1,27 @@ +#include "sm2335.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm2335 { + +static const char *const TAG = "sm2335"; + +void SM2335::setup() { + ESP_LOGCONFIG(TAG, "Setting up sm2335 Output Component..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->pwm_amounts_.resize(5, 0); +} + +void SM2335::dump_config() { + ESP_LOGCONFIG(TAG, "sm2335:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Color Channels Max Power: %u", this->max_power_color_channels_); + ESP_LOGCONFIG(TAG, " White Channels Max Power: %u", this->max_power_white_channels_); +} + +} // namespace sm2335 +} // namespace esphome diff --git a/esphome/components/sm2335/sm2335.h b/esphome/components/sm2335/sm2335.h new file mode 100644 index 0000000000..c8cf825189 --- /dev/null +++ b/esphome/components/sm2335/sm2335.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sm10bit_base/sm10bit_base.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sm2335 { + +class SM2335 : public sm10bit_base::Sm10BitBase { + public: + SM2335() = default; + + void setup() override; + void dump_config() override; +}; + +} // namespace sm2335 +} // namespace esphome diff --git a/esphome/components/tee501/__init__.py b/esphome/components/tee501/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tee501/sensor.py b/esphome/components/tee501/sensor.py new file mode 100644 index 0000000000..329fc724bd --- /dev/null +++ b/esphome/components/tee501/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +tee501_ns = cg.esphome_ns.namespace("tee501") + +TEE501Component = tee501_ns.class_( + "TEE501Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + TEE501Component, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +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/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp new file mode 100644 index 0000000000..22329d40cd --- /dev/null +++ b/esphome/components/tee501/tee501.cpp @@ -0,0 +1,85 @@ +#include "tee501.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tee501 { + +static const char *const TAG = "tee501"; + +void TEE501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TEE501..."); + uint8_t address[] = {0x70, 0x29}; + this->write(address, 2, false); + uint8_t identification[9]; + this->read(identification, 9); + if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +} + +void TEE501Component::dump_config() { + ESP_LOGCONFIG(TAG, "TEE501:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with TEE501 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "TEE501", this); +} + +float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } +void TEE501Component::update() { + uint8_t address_1[] = {0x2C, 0x1B}; + this->write(address_1, 2, true); + this->set_timeout(50, [this]() { + uint8_t i2c_response[3]; + this->read(i2c_response, 3); + if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return; + } + float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]); + if (temperature > 55536) { + temperature = (temperature - 65536) / 100; + } else { + temperature = temperature / 100; + } + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + }); +} + +unsigned char TEE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { + unsigned char crc_val = 0xFF; + unsigned char i = 0; + unsigned char j = 0; + for (i = from; i <= to; i++) { + int cur_val = buf[i]; + for (j = 0; j < 8; j++) { + if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal + { + crc_val = ((crc_val << 1) ^ 0x31); + } else { + crc_val = (crc_val << 1); + } + cur_val = cur_val << 1; + } + } + return crc_val; +} + +} // namespace tee501 +} // namespace esphome diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h new file mode 100644 index 0000000000..fc655e58c9 --- /dev/null +++ b/esphome/components/tee501/tee501.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tee501 { + +/// This class implements support for the tee501 of temperature i2c sensors. +class TEE501Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace tee501 +} // namespace esphome diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index b3e545d3e9..5db346b99f 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -43,15 +43,16 @@ void TemplateSwitch::setup() { if (!this->restore_state_) return; - auto restored = this->get_initial_state(); - if (!restored.has_value()) - return; + optional initial_state = this->get_initial_state_with_restore_mode(); - ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); - if (*restored) { - this->turn_on(); - } else { - this->turn_off(); + if (initial_state.has_value()) { + ESP_LOGD(TAG, " Restored state %s", ONOFF(initial_state.value())); + // if it has a value, restore_mode is not "DISABLED", therefore act on the switch: + if (initial_state.value()) { + this->turn_on(); + } else { + this->turn_off(); + } } } void TemplateSwitch::dump_config() { diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 8aa61dbb93..9a57f6a337 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_QUIET_ACTION, CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, CONF_FAN_ONLY_COOLING, @@ -273,6 +274,7 @@ def validate_thermostat(config): CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_QUIET_ACTION, ], } for req_config_item, config_triggers in requirements.items(): @@ -413,6 +415,7 @@ def validate_thermostat(config): "MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION], "FOCUS": [CONF_FAN_MODE_FOCUS_ACTION], "DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION], + "QUIET": [CONF_FAN_MODE_QUIET_ACTION], } for preset_config in config[CONF_PRESET]: @@ -500,12 +503,13 @@ def validate_thermostat(config): CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_QUIET_ACTION, ] for config_req_action in requirements: if config_req_action in config: return config raise cv.Invalid( - f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" + f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION}, {CONF_FAN_MODE_QUIET_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" ) return config @@ -563,6 +567,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation( single=True ), + cv.Optional(CONF_FAN_MODE_QUIET_ACTION): automation.validate_automation( + single=True + ), cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation( single=True ), @@ -836,6 +843,11 @@ async def to_code(config): var.get_fan_mode_diffuse_trigger(), [], config[CONF_FAN_MODE_DIFFUSE_ACTION] ) cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_FAN_MODE_QUIET_ACTION in config: + await automation.build_automation( + var.get_fan_mode_quiet_trigger(), [], config[CONF_FAN_MODE_QUIET_ACTION] + ) + cg.add(var.set_supports_fan_mode_quiet(True)) if CONF_SWING_BOTH_ACTION in config: await automation.build_automation( var.get_swing_mode_both_trigger(), [], config[CONF_SWING_BOTH_ACTION] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 1fbbbe022c..51da663a0c 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -247,6 +247,8 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); if (supports_fan_mode_diffuse_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); + if (supports_fan_mode_quiet_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET); if (supports_swing_mode_both_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); @@ -594,6 +596,10 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo trig = this->fan_mode_diffuse_trigger_; ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); break; + case climate::CLIMATE_FAN_QUIET: + trig = this->fan_mode_quiet_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_QUIET mode"); + break; default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value @@ -1093,6 +1099,7 @@ ThermostatClimate::ThermostatClimate() fan_mode_middle_trigger_(new Trigger<>()), fan_mode_focus_trigger_(new Trigger<>()), fan_mode_diffuse_trigger_(new Trigger<>()), + fan_mode_quiet_trigger_(new Trigger<>()), swing_mode_both_trigger_(new Trigger<>()), swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), @@ -1208,6 +1215,9 @@ void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; } +void ThermostatClimate::set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) { + this->supports_fan_mode_quiet_ = supports_fan_mode_quiet; +} void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { this->supports_swing_mode_both_ = supports_swing_mode_both; } @@ -1250,6 +1260,7 @@ Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->f Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() const { return this->fan_mode_quiet_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } @@ -1294,7 +1305,8 @@ void ThermostatClimate::dump_config() { } if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || - this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_) { + this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || + this->supports_fan_mode_quiet_) { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %us", this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); } @@ -1323,6 +1335,7 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE QUIET: %s", YESNO(this->supports_fan_mode_quiet_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 124f95de33..677b4ad324 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -101,6 +101,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_fan_mode_middle(bool supports_fan_mode_middle); void set_supports_fan_mode_focus(bool supports_fan_mode_focus); void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_fan_mode_quiet(bool supports_fan_mode_quiet); void set_supports_swing_mode_both(bool supports_swing_mode_both); void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); void set_supports_swing_mode_off(bool supports_swing_mode_off); @@ -132,6 +133,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_fan_mode_middle_trigger() const; Trigger<> *get_fan_mode_focus_trigger() const; Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_fan_mode_quiet_trigger() const; Trigger<> *get_swing_mode_both_trigger() const; Trigger<> *get_swing_mode_horizontal_trigger() const; Trigger<> *get_swing_mode_off_trigger() const; @@ -277,6 +279,7 @@ class ThermostatClimate : public climate::Climate, public Component { bool supports_fan_mode_middle_{false}; bool supports_fan_mode_focus_{false}; bool supports_fan_mode_diffuse_{false}; + bool supports_fan_mode_quiet_{false}; /// Whether the controller supports various swing modes. /// @@ -372,6 +375,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the controller should switch the fan to "diffuse" position. Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + /// The trigger to call when the controller should switch the fan to "quiet" position. + Trigger<> *fan_mode_quiet_trigger_{nullptr}; + /// The trigger to call when the controller should switch the swing mode to "both". Trigger<> *swing_mode_both_trigger_{nullptr}; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 104e885aab..a070ccceb2 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -124,9 +124,6 @@ void ToshibaClimate::setup() { // Set supported modes & temperatures based on model this->minimum_temperature_ = this->temperature_min_(); this->maximum_temperature_ = this->temperature_max_(); - this->supports_dry_ = this->toshiba_supports_dry_(); - this->supports_fan_only_ = this->toshiba_supports_fan_only_(); - this->fan_modes_ = this->toshiba_fan_modes_(); this->swing_modes_ = this->toshiba_swing_modes_(); // Never send nan to HA if (std::isnan(this->target_temperature)) @@ -178,12 +175,39 @@ void ToshibaClimate::transmit_generic_() { mode = TOSHIBA_MODE_COOL; break; + case climate::CLIMATE_MODE_DRY: + mode = TOSHIBA_MODE_DRY; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + mode = TOSHIBA_MODE_FAN_ONLY; + break; + case climate::CLIMATE_MODE_HEAT_COOL: default: mode = TOSHIBA_MODE_AUTO; } - message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; + uint8_t fan; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan = TOSHIBA_FAN_SPEED_1; + break; + + case climate::CLIMATE_FAN_MEDIUM: + fan = TOSHIBA_FAN_SPEED_3; + break; + + case climate::CLIMATE_FAN_HIGH: + fan = TOSHIBA_FAN_SPEED_5; + break; + + case climate::CLIMATE_FAN_AUTO: + default: + fan = TOSHIBA_FAN_SPEED_AUTO; + break; + } + message[6] = fan | mode; // Zero message[7] = 0x00; diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index 36e8760169..729548e747 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -22,7 +22,10 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0; class ToshibaClimate : public climate_ir::ClimateIR { public: - ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f) {} + 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}) {} void setup() override; void set_model(Model model) { this->model_ = model; } @@ -46,18 +49,6 @@ class ToshibaClimate : public climate_ir::ClimateIR { float temperature_max_() { return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX; } - bool toshiba_supports_dry_() { - return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); - } - bool toshiba_supports_fan_only_() { - return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); - } - std::set toshiba_fan_modes_() { - return (this->model_ == MODEL_GENERIC) - ? std::set{} - : std::set{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}; - } std::set toshiba_swing_modes_() { return (this->model_ == MODEL_GENERIC) ? std::set{} diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py index 63a0733365..5435ed4b62 100644 --- a/esphome/components/tsl2591/sensor.py +++ b/esphome/components/tsl2591/sensor.py @@ -24,6 +24,7 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_GAIN, + CONF_ACTUAL_GAIN, CONF_ID, CONF_NAME, CONF_INTEGRATION_TIME, @@ -79,7 +80,6 @@ TSL2591Component = tsl2591_ns.class_( "TSL2591Component", cg.PollingComponent, i2c.I2CDevice ) - CONFIG_SCHEMA = ( cv.Schema( { @@ -106,6 +106,12 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ACTUAL_GAIN): sensor.sensor_schema( + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional( CONF_INTEGRATION_TIME, default="100ms" ): validate_integration_time, @@ -150,6 +156,11 @@ async def to_code(config): sens = await sensor.new_sensor(conf) cg.add(var.set_calculated_lux_sensor(sens)) + if CONF_ACTUAL_GAIN in config: + conf = config[CONF_ACTUAL_GAIN] + sens = await sensor.new_sensor(conf) + cg.add(var.set_actual_gain_sensor(sens)) + cg.add(var.set_name(config[CONF_NAME])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index f8c59a53c6..5086a38408 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -130,6 +130,7 @@ void TSL2591Component::dump_config() { LOG_SENSOR(" ", "Infrared:", this->infrared_sensor_); LOG_SENSOR(" ", "Visible:", this->visible_sensor_); LOG_SENSOR(" ", "Calculated lux:", this->calculated_lux_sensor_); + LOG_SENSOR(" ", "Actual gain:", this->actual_gain_sensor_); LOG_UPDATE_INTERVAL(this); } @@ -140,8 +141,9 @@ void TSL2591Component::process_update_() { uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined); uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined); float lux = this->get_calculated_lux(full, infrared); - ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f", combined, full, infrared, - visible, lux); + uint16_t actual_gain = this->get_actual_gain(); + ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f. Actual gain: %d.", combined, + full, infrared, visible, lux, actual_gain); if (this->full_spectrum_sensor_ != nullptr) { this->full_spectrum_sensor_->publish_state(full); } @@ -154,9 +156,14 @@ void TSL2591Component::process_update_() { if (this->calculated_lux_sensor_ != nullptr) { this->calculated_lux_sensor_->publish_state(lux); } + if (this->component_gain_ == TSL2591_CGAIN_AUTO) { this->automatic_gain_update(full); } + + if (this->actual_gain_sensor_ != nullptr) { + this->actual_gain_sensor_->publish_state(actual_gain); + } this->status_clear_warning(); } @@ -207,6 +214,10 @@ void TSL2591Component::set_calculated_lux_sensor(sensor::Sensor *calculated_lux_ this->calculated_lux_sensor_ = calculated_lux_sensor; } +void TSL2591Component::set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor) { + this->actual_gain_sensor_ = actual_gain_sensor; +} + void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_time) { this->integration_time_ = integration_time; } @@ -347,8 +358,8 @@ float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infr uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535); if ((full_spectrum == max_count) || (infrared == max_count)) { // Signal an overflow - ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain.", this->name_); - return -1.0F; + ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain or integration time.", this->name_); + return NAN; } if ((full_spectrum == 0) && (infrared == 0)) { @@ -377,7 +388,6 @@ float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infr again = 1.0F; break; } - // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly. // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14 // and that library code. @@ -448,5 +458,25 @@ void TSL2591Component::automatic_gain_update(uint16_t full_spectrum) { ESP_LOGD(TAG, "Gain setting: %d", this->gain_); } +/** Reads the actual gain used + * + * Useful for exposing the real gain used when configured in "auto" gain mode + */ +float TSL2591Component::get_actual_gain() { + switch (this->gain_) { + case TSL2591_GAIN_LOW: + return 1.0F; + case TSL2591_GAIN_MED: + return 25.0F; + case TSL2591_GAIN_HIGH: + return 400.0F; + case TSL2591_GAIN_MAX: + return 9500.0F; + default: + // Shouldn't get here, but just in case. + return NAN; + } +} + } // namespace tsl2591 } // namespace esphome diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index 5b7eea35ec..d7c5230276 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -217,14 +217,21 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { * * This gets called on update and tries to keep the ADC readings in the middle of the range */ - void automatic_gain_update(uint16_t full_spectrum); + /** Reads the actual gain used + * + * Useful for exposing the real gain used when configured in "auto" gain mode + */ + float get_actual_gain(); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these. They're for ESPHome integration use.) /** Used by ESPHome framework. */ void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor); /** Used by ESPHome framework. */ + void set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor); + /** Used by ESPHome framework. */ void set_infrared_sensor(sensor::Sensor *infrared_sensor); /** Used by ESPHome framework. */ void set_visible_sensor(sensor::Sensor *visible_sensor); @@ -249,6 +256,7 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *infrared_sensor_{nullptr}; sensor::Sensor *visible_sensor_{nullptr}; sensor::Sensor *calculated_lux_sensor_{nullptr}; + sensor::Sensor *actual_gain_sensor_{nullptr}; TSL2591IntegrationTime integration_time_; TSL2591ComponentGain component_gain_; TSL2591Gain gain_; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 7b580986e1..89a687e5a6 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -66,7 +66,6 @@ void Tuya::dump_config() { LOG_PIN(" Status Pin: ", this->status_pin_.value()); } ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); - this->check_uart_settings(9600); } bool Tuya::validate_message_() { diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 01a1049568..ed60a9f880 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -246,11 +246,13 @@ def final_validate_device_schema( baud_rate: Optional[int] = None, require_tx: bool = False, require_rx: bool = False, + parity: Optional[str] = None, + stop_bits: Optional[int] = None, ): def validate_baud_rate(value): if value != baud_rate: raise cv.Invalid( - f"Component {name} required baud rate {baud_rate} for the uart bus" + f"Component {name} requires baud rate {baud_rate} for the uart bus" ) return value @@ -266,6 +268,20 @@ def final_validate_device_schema( return validator + def validate_parity(value): + if value != parity: + raise cv.Invalid( + f"Component {name} requires parity {parity} for the uart bus" + ) + return value + + def validate_stop_bits(value): + if value != stop_bits: + raise cv.Invalid( + f"Component {name} requires stop bits {stop_bits} for the uart bus" + ) + return value + def validate_hub(hub_config): hub_schema = {} uart_id = hub_config[CONF_ID] @@ -288,6 +304,10 @@ def final_validate_device_schema( ] = validate_pin(CONF_RX_PIN, device) if baud_rate is not None: hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate + if parity is not None: + hub_schema[cv.Required(CONF_PARITY)] = validate_parity + if stop_bits is not None: + hub_schema[cv.Required(CONF_STOP_BITS)] = validate_stop_bits return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config) return cv.Schema( diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 07d7d8f2cf..50e584f5d5 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, - STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, DEVICE_CLASS_DURATION, @@ -17,7 +16,6 @@ CONFIG_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_SECOND, icon=ICON_TIMER, accuracy_decimals=0, - state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_DURATION, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py new file mode 100644 index 0000000000..70f130e23b --- /dev/null +++ b/esphome/components/vbus/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@ssieb"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +vbus_ns = cg.esphome_ns.namespace("vbus") +VBus = vbus_ns.class_("VBus", uart.UARTDevice, cg.Component) + +CONF_VBUS_ID = "vbus_id" + +CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" +CONF_DELTASOL_C = "deltasol_c" +CONF_DELTASOL_CS2 = "deltasol_cs2" +CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" + +CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VBus), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py new file mode 100644 index 0000000000..9901fb2724 --- /dev/null +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -0,0 +1,296 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, + CONF_BINARY_SENSORS, + CONF_COMMAND, + CONF_CUSTOM, + CONF_DEST, + CONF_LAMBDA, + CONF_MODEL, + CONF_SOURCE, + DEVICE_CLASS_PROBLEM, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from .. import ( + vbus_ns, + VBus, + CONF_VBUS_ID, + CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_C, + CONF_DELTASOL_CS2, + CONF_DELTASOL_CS_PLUS, +) + +DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) +DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) +DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) +DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) +VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component) +VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component) + +CONF_RELAY1 = "relay1" +CONF_RELAY2 = "relay2" +CONF_SENSOR1_ERROR = "sensor1_error" +CONF_SENSOR2_ERROR = "sensor2_error" +CONF_SENSOR3_ERROR = "sensor3_error" +CONF_SENSOR4_ERROR = "sensor4_error" +CONF_COLLECTOR_MAX = "collector_max" +CONF_COLLECTOR_MIN = "collector_min" +CONF_COLLECTOR_FROST = "collector_frost" +CONF_TUBE_COLLECTOR = "tube_collector" +CONF_RECOOLING = "recooling" +CONF_HQM = "hqm" + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_DELTASOL_BS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_RELAY1): binary_sensor.binary_sensor_schema(), + cv.Optional(CONF_RELAY2): binary_sensor.binary_sensor_schema(), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_COLLECTOR_MAX): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_COLLECTOR_MIN): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_COLLECTOR_FROST): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TUBE_COLLECTOR): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_RECOOLING): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_HQM): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_C), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_CUSTOM: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VBusCustom), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_COMMAND): cv.uint16_t, + cv.Optional(CONF_SOURCE): cv.uint16_t, + cv.Optional(CONF_DEST): cv.uint16_t, + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + binary_sensor.binary_sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(VBusCustomSub), + cv.Required(CONF_LAMBDA): cv.lambda_, + } + ) + ), + } + ), + }, + key=CONF_MODEL, + lower=True, + space="_", +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if config[CONF_MODEL] == CONF_DELTASOL_BS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4221)) + cg.add(var.set_dest(0x0010)) + if CONF_RELAY1 in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_RELAY1]) + cg.add(var.set_relay1_bsensor(sens)) + if CONF_RELAY2 in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_RELAY2]) + cg.add(var.set_relay2_bsensor(sens)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + if CONF_COLLECTOR_MAX in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_MAX]) + cg.add(var.set_collector_max_bsensor(sens)) + if CONF_COLLECTOR_MIN in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_MIN]) + cg.add(var.set_collector_min_bsensor(sens)) + if CONF_COLLECTOR_FROST in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_FROST]) + cg.add(var.set_collector_frost_bsensor(sens)) + if CONF_TUBE_COLLECTOR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_TUBE_COLLECTOR]) + cg.add(var.set_tube_collector_bsensor(sens)) + if CONF_RECOOLING in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_RECOOLING]) + cg.add(var.set_recooling_bsensor(sens)) + if CONF_HQM in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HQM]) + cg.add(var.set_hqm_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_C: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4212)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x1121)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x2211)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + + elif config[CONF_MODEL] == CONF_CUSTOM: + if CONF_COMMAND in config: + cg.add(var.set_command(config[CONF_COMMAND])) + if CONF_SOURCE in config: + cg.add(var.set_source(config[CONF_SOURCE])) + if CONF_DEST in config: + cg.add(var.set_dest(config[CONF_DEST])) + bsensors = [] + for conf in config[CONF_BINARY_SENSORS]: + bsens = await binary_sensor.new_binary_sensor(conf) + lambda_ = await cg.process_lambda( + conf[CONF_LAMBDA], + [(cg.std_vector.template(cg.uint8), "x")], + return_type=cg.bool_, + ) + cg.add(bsens.set_message_parser(lambda_)) + bsensors.append(bsens) + cg.add(var.set_bsensors(bsensors)) + + vbus = await cg.get_variable(config[CONF_VBUS_ID]) + cg.add(vbus.register_listener(var)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp new file mode 100644 index 0000000000..6edbae22ba --- /dev/null +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -0,0 +1,142 @@ +#include "vbus_binary_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace vbus { + +static const char *const TAG = "vbus.binary_sensor"; + +void DeltaSolBSPlusBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS Plus:"); + LOG_BINARY_SENSOR(" ", "Relay 1 On", this->relay1_bsensor_); + LOG_BINARY_SENSOR(" ", "Relay 2 On", this->relay2_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Collector Max", this->collector_max_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Collector Min", this->collector_min_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Collector Frost", this->collector_frost_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Tube Collector", this->tube_collector_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Recooling", this->recooling_bsensor_); + LOG_BINARY_SENSOR(" ", "Option Heat Quantity Measurement", this->hqm_bsensor_); +} + +void DeltaSolBSPlusBSensor::handle_message(std::vector &message) { + if (this->relay1_bsensor_ != nullptr) + this->relay1_bsensor_->publish_state(message[10] & 1); + if (this->relay2_bsensor_ != nullptr) + this->relay2_bsensor_->publish_state(message[10] & 2); + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[11] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[11] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[11] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[11] & 8); + if (this->collector_max_bsensor_ != nullptr) + this->collector_max_bsensor_->publish_state(message[15] & 1); + if (this->collector_min_bsensor_ != nullptr) + this->collector_min_bsensor_->publish_state(message[15] & 2); + if (this->collector_frost_bsensor_ != nullptr) + this->collector_frost_bsensor_->publish_state(message[15] & 4); + if (this->tube_collector_bsensor_ != nullptr) + this->tube_collector_bsensor_->publish_state(message[15] & 8); + if (this->recooling_bsensor_ != nullptr) + this->recooling_bsensor_->publish_state(message[15] & 0x10); + if (this->hqm_bsensor_ != nullptr) + this->hqm_bsensor_->publish_state(message[15] & 0x20); +} + +void DeltaSolCBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol C:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCBSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[10] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[10] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[10] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[10] & 8); +} + +void DeltaSolCS2BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS2:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCS2BSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[18] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[18] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[18] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[18] & 8); +} + +void DeltaSolCSPlusBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS Plus:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCSPlusBSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[20] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[20] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[20] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[20] & 8); +} + +void VBusCustomBSensor::dump_config() { + ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:"); + if (this->source_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Source address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Source address: 0x%04x", this->source_); + } + if (this->dest_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Dest address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Dest address: 0x%04x", this->dest_); + } + if (this->command_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Command: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Command: 0x%04x", this->command_); + } + ESP_LOGCONFIG(TAG, " Binary Sensors:"); + for (VBusCustomSubBSensor *bsensor : this->bsensors_) + LOG_BINARY_SENSOR(" ", "-", bsensor); +} + +void VBusCustomBSensor::handle_message(std::vector &message) { + for (VBusCustomSubBSensor *bsensor : this->bsensors_) + bsensor->parse_message(message); +} + +void VBusCustomSubBSensor::parse_message(std::vector &message) { + this->publish_state(this->message_parser_(message)); +} + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h new file mode 100644 index 0000000000..c0a823a0ab --- /dev/null +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -0,0 +1,115 @@ +#pragma once + +#include "../vbus.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace vbus { + +class DeltaSolBSPlusBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_relay1_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay1_bsensor_ = bsensor; } + void set_relay2_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay2_bsensor_ = bsensor; } + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + void set_collector_max_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_max_bsensor_ = bsensor; } + void set_collector_min_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_min_bsensor_ = bsensor; } + void set_collector_frost_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_frost_bsensor_ = bsensor; } + void set_tube_collector_bsensor(binary_sensor::BinarySensor *bsensor) { this->tube_collector_bsensor_ = bsensor; } + void set_recooling_bsensor(binary_sensor::BinarySensor *bsensor) { this->recooling_bsensor_ = bsensor; } + void set_hqm_bsensor(binary_sensor::BinarySensor *bsensor) { this->hqm_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *relay1_bsensor_{nullptr}; + binary_sensor::BinarySensor *relay2_bsensor_{nullptr}; + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *collector_max_bsensor_{nullptr}; + binary_sensor::BinarySensor *collector_min_bsensor_{nullptr}; + binary_sensor::BinarySensor *collector_frost_bsensor_{nullptr}; + binary_sensor::BinarySensor *tube_collector_bsensor_{nullptr}; + binary_sensor::BinarySensor *recooling_bsensor_{nullptr}; + binary_sensor::BinarySensor *hqm_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCS2BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCSPlusBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubBSensor; + +class VBusCustomBSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_bsensors(std::vector bsensors) { this->bsensors_ = std::move(bsensors); }; + + protected: + std::vector bsensors_; + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubBSensor : public binary_sensor::BinarySensor, public Component { + public: + void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); }; + void parse_message(std::vector &message); + + protected: + message_parser_t message_parser_; +}; + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py new file mode 100644 index 0000000000..bce28758ce --- /dev/null +++ b/esphome/components/vbus/sensor/__init__.py @@ -0,0 +1,568 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_COMMAND, + CONF_CUSTOM, + CONF_DEST, + CONF_LAMBDA, + CONF_MODEL, + CONF_SENSORS, + CONF_SOURCE, + CONF_TIME, + CONF_VERSION, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_PERCENT, + ICON_RADIATOR, + ICON_THERMOMETER, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HOUR, + UNIT_MINUTE, + UNIT_PERCENT, + UNIT_WATT_HOURS, +) +from .. import ( + vbus_ns, + VBus, + CONF_VBUS_ID, + CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_C, + CONF_DELTASOL_CS2, + CONF_DELTASOL_CS_PLUS, +) + +DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) +DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) +DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) +DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) +VBusCustom = vbus_ns.class_("VBusCustomSensor", cg.Component) +VBusCustomSub = vbus_ns.class_("VBusCustomSubSensor", cg.Component) + +CONF_FLOW_RATE = "flow_rate" +CONF_HEAT_QUANTITY = "heat_quantity" +CONF_OPERATING_HOURS = "operating_hours" +CONF_OPERATING_HOURS_1 = "operating_hours_1" +CONF_OPERATING_HOURS_2 = "operating_hours_2" +CONF_PUMP_SPEED = "pump_speed" +CONF_PUMP_SPEED_1 = "pump_speed_1" +CONF_PUMP_SPEED_2 = "pump_speed_2" +CONF_TEMPERATURE_1 = "temperature_1" +CONF_TEMPERATURE_2 = "temperature_2" +CONF_TEMPERATURE_3 = "temperature_3" +CONF_TEMPERATURE_4 = "temperature_4" +CONF_TEMPERATURE_5 = "temperature_5" + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_DELTASOL_BS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_C), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_5): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_FLOW_RATE): sensor.sensor_schema( + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ), + CONF_CUSTOM: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(VBusCustom), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_COMMAND): cv.uint16_t, + cv.Optional(CONF_SOURCE): cv.uint16_t, + cv.Optional(CONF_DEST): cv.uint16_t, + cv.Optional(CONF_SENSORS): cv.ensure_list( + sensor.sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(VBusCustomSub), + cv.Required(CONF_LAMBDA): cv.lambda_, + } + ) + ), + } + ), + }, + key=CONF_MODEL, + lower=True, + space="_", +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if config[CONF_MODEL] == CONF_DELTASOL_BS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4221)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_C: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4212)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x1121)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED]) + cg.add(var.set_pump_speed_sensor(sens)) + if CONF_OPERATING_HOURS in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS]) + cg.add(var.set_operating_hours_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + + if config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x2211)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_TEMPERATURE_5 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_5]) + cg.add(var.set_temperature5_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + if CONF_FLOW_RATE in config: + sens = await sensor.new_sensor(config[CONF_FLOW_RATE]) + cg.add(var.set_flow_rate_sensor(sens)) + + elif config[CONF_MODEL] == CONF_CUSTOM: + if CONF_COMMAND in config: + cg.add(var.set_command(config[CONF_COMMAND])) + if CONF_SOURCE in config: + cg.add(var.set_source(config[CONF_SOURCE])) + if CONF_DEST in config: + cg.add(var.set_dest(config[CONF_DEST])) + sensors = [] + for conf in config[CONF_SENSORS]: + sens = await sensor.new_sensor(conf) + lambda_ = await cg.process_lambda( + conf[CONF_LAMBDA], + [(cg.std_vector.template(cg.uint8), "x")], + return_type=cg.float_, + ) + cg.add(sens.set_message_parser(lambda_)) + sensors.append(sens) + cg.add(var.set_sensors(sensors)) + + vbus = await cg.get_variable(config[CONF_VBUS_ID]) + cg.add(vbus.register_listener(var)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp new file mode 100644 index 0000000000..57d5a355ad --- /dev/null +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -0,0 +1,208 @@ +#include "vbus_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace vbus { + +static const char *const TAG = "vbus.sensor"; + +static inline uint16_t get_u16(std::vector &message, int start) { + return (message[start + 1] << 8) + message[start]; +} + +static inline int16_t get_i16(std::vector &message, int start) { + return (int16_t)((message[start + 1] << 8) + message[start]); +} + +void DeltaSolBSPlusSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS Plus:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBSPlusSensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 16)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 18)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state(get_u16(message, 20) + get_u16(message, 22) * 1000 + + get_u16(message, 24) * 1000000); + } + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 12)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); +} + +void DeltaSolCSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol C:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); +} + +void DeltaSolCSensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 12)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state(get_u16(message, 16) + get_u16(message, 18) * 1000 + + get_u16(message, 20) * 1000000); + } + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 22)); +} + +void DeltaSolCS2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS2:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed", this->pump_speed_sensor_); + LOG_SENSOR(" ", "Operating Hours", this->operating_hours_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolCS2Sensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed_sensor_ != nullptr) + this->pump_speed_sensor_->publish_state(message[12]); + if (this->operating_hours_sensor_ != nullptr) + this->operating_hours_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) + this->heat_quantity_sensor_->publish_state((get_u16(message, 26) << 16) + get_u16(message, 24)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f); +} + +void DeltaSolCSPlusSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS Plus:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Temperature 5", this->temperature5_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); + LOG_SENSOR(" ", "Flow Rate", this->flow_rate_sensor_); +} + +void DeltaSolCSPlusSensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->temperature5_sensor_ != nullptr) + this->temperature5_sensor_->publish_state(get_i16(message, 36) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[12]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) + this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 12)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); + if (this->flow_rate_sensor_ != nullptr) + this->flow_rate_sensor_->publish_state(get_u16(message, 38)); +} + +void VBusCustomSensor::dump_config() { + ESP_LOGCONFIG(TAG, "VBus Custom Sensor:"); + if (this->source_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Source address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Source address: 0x%04x", this->source_); + } + if (this->dest_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Dest address: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Dest address: 0x%04x", this->dest_); + } + if (this->command_ == 0xffff) { + ESP_LOGCONFIG(TAG, " Command: ANY"); + } else { + ESP_LOGCONFIG(TAG, " Command: 0x%04x", this->command_); + } + ESP_LOGCONFIG(TAG, " Sensors:"); + for (VBusCustomSubSensor *sensor : this->sensors_) + LOG_SENSOR(" ", "-", sensor); +} + +void VBusCustomSensor::handle_message(std::vector &message) { + for (VBusCustomSubSensor *sensor : this->sensors_) + sensor->parse_message(message); +} + +void VBusCustomSubSensor::parse_message(std::vector &message) { + this->publish_state(this->message_parser_(message)); +} + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h new file mode 100644 index 0000000000..6ba752b68c --- /dev/null +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -0,0 +1,151 @@ +#pragma once + +#include "../vbus.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace vbus { + +class DeltaSolBSPlusSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCS2Sensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed_sensor(sensor::Sensor *sensor) { this->pump_speed_sensor_ = sensor; } + void set_operating_hours_sensor(sensor::Sensor *sensor) { this->operating_hours_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed_sensor_{nullptr}; + sensor::Sensor *operating_hours_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class DeltaSolCSPlusSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_temperature5_sensor(sensor::Sensor *sensor) { this->temperature5_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + void set_flow_rate_sensor(sensor::Sensor *sensor) { this->flow_rate_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *temperature5_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + sensor::Sensor *flow_rate_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubSensor; + +class VBusCustomSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_sensors(std::vector sensors) { this->sensors_ = std::move(sensors); }; + + protected: + std::vector sensors_; + void handle_message(std::vector &message) override; +}; + +class VBusCustomSubSensor : public sensor::Sensor, public Component { + public: + void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); }; + void parse_message(std::vector &message); + + protected: + message_parser_t message_parser_; +}; + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp new file mode 100644 index 0000000000..c9758891cc --- /dev/null +++ b/esphome/components/vbus/vbus.cpp @@ -0,0 +1,124 @@ +#include "vbus.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace vbus { + +static const char *const TAG = "vbus"; + +void VBus::dump_config() { + ESP_LOGCONFIG(TAG, "VBus:"); + check_uart_settings(9600); +} + +static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) { + for (int i = 0; i < count; i++, septet >>= 1) { + if (septet & 1) + data[start + i] |= 0x80; + } +} + +static bool checksum(const uint8_t *data, int start, int count) { + uint8_t csum = 0x7f; + for (int i = 0; i < count; i++) + csum = (csum - data[start + i]) & 0x7f; + return csum == 0; +} + +void VBus::loop() { + if (!available()) + return; + + while (available()) { + uint8_t c; + read_byte(&c); + + if (c == 0xaa) { + this->state_ = 1; + this->buffer_.clear(); + continue; + } + if (c & 0x80) { + this->state_ = 0; + continue; + } + if (this->state_ == 0) + continue; + + if (this->state_ == 1) { + this->buffer_.push_back(c); + if (this->buffer_.size() == 7) { + this->protocol_ = this->buffer_[4]; + this->source_ = (this->buffer_[3] << 8) + this->buffer_[2]; + this->dest_ = (this->buffer_[1] << 8) + this->buffer_[0]; + this->command_ = (this->buffer_[6] << 8) + this->buffer_[5]; + } + if ((this->protocol_ == 0x20) && (this->buffer_.size() == 15)) { + this->state_ = 0; + if (!checksum(this->buffer_.data(), 0, 15)) { + ESP_LOGE(TAG, "P2 checksum failed"); + continue; + } + septet_spread(this->buffer_.data(), 7, 6, this->buffer_[13]); + uint16_t id = (this->buffer_[8] << 8) + this->buffer_[7]; + uint32_t value = + (this->buffer_[12] << 24) + (this->buffer_[11] << 16) + (this->buffer_[10] << 8) + this->buffer_[9]; + ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04x (%d)", this->command_, this->source_, this->dest_, id, value, + value); + } else if ((this->protocol_ == 0x10) && (this->buffer_.size() == 9)) { + if (!checksum(this->buffer_.data(), 0, 9)) { + ESP_LOGE(TAG, "P1 checksum failed"); + this->state_ = 0; + continue; + } + this->frames_ = this->buffer_[7]; + if (this->frames_) { + this->state_ = 2; + this->cframe_ = 0; + this->fbcount_ = 0; + this->buffer_.clear(); + } else { + this->state_ = 0; + ESP_LOGD(TAG, "P1 empty message"); + } + } + continue; + } + + if (this->state_ == 2) { + this->fbytes_[this->fbcount_++] = c; + if (this->fbcount_ < 6) + continue; + this->fbcount_ = 0; + if (!checksum(this->fbytes_, 0, 6)) { + ESP_LOGE(TAG, "frame checksum failed"); + continue; + } + septet_spread(this->fbytes_, 0, 4, this->fbytes_[4]); + for (int i = 0; i < 4; i++) + this->buffer_.push_back(this->fbytes_[i]); + if (++this->cframe_ < this->frames_) + continue; + ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_, + format_hex(this->buffer_).c_str()); + for (auto &listener : this->listeners_) + listener->on_message(this->command_, this->source_, this->dest_, this->buffer_); + this->state_ = 0; + continue; + } + } +} + +void VBusListener::on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector &message) { + if ((this->command_ != 0xffff) && (this->command_ != command)) + return; + if ((this->source_ != 0xffff) && (this->source_ != source)) + return; + if ((this->dest_ != 0xffff) && (this->dest_ != dest)) + return; + this->handle_message(message); +} + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h new file mode 100644 index 0000000000..7e97b5049a --- /dev/null +++ b/esphome/components/vbus/vbus.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace vbus { + +using message_parser_t = std::function &)>; + +class VBus; + +class VBusListener { + public: + void set_command(uint16_t command) { this->command_ = command; } + void set_source(uint16_t source) { this->source_ = source; } + void set_dest(uint16_t dest) { this->dest_ = dest; } + + void on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector &message); + + protected: + uint16_t command_{0xffff}; + uint16_t source_{0xffff}; + uint16_t dest_{0xffff}; + + virtual void handle_message(std::vector &message) = 0; +}; + +class VBus : public uart::UARTDevice, public Component { + public: + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); } + + protected: + int state_{0}; + std::vector buffer_; + uint8_t protocol_; + uint16_t source_; + uint16_t dest_; + uint16_t command_; + uint8_t frames_; + uint8_t cframe_; + uint8_t fbytes_[6]; + int fbcount_; + std::vector listeners_{}; +}; + +} // namespace vbus +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 30ac959e43..513399e257 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2,12 +2,12 @@ #include "web_server.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" -#include "esphome/core/entity_base.h" -#include "esphome/core/util.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" #include "StreamString.h" @@ -83,6 +83,13 @@ UrlMatch match_url(const std::string &url, bool only_domain = false) { return match; } +WebServer::WebServer(web_server_base::WebServerBase *base) + : base_(base), entities_iterator_(ListEntitiesIterator(this)) { +#ifdef USE_ESP32 + to_schedule_lock_ = xSemaphoreCreateMutex(); +#endif +} + void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } @@ -97,7 +104,8 @@ void WebServer::setup() { // Configure reconnect timeout and send config client->send(json::build_json([this](JsonObject root) { - root["title"] = App.get_name(); + root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root["comment"] = App.get_comment(); root["ota"] = this->allow_ota_; root["lang"] = "en"; }).c_str(), @@ -120,7 +128,25 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } -void WebServer::loop() { this->entities_iterator_.advance(); } +void WebServer::loop() { +#ifdef USE_ESP32 + if (xSemaphoreTake(this->to_schedule_lock_, 0L)) { + std::function fn; + if (!to_schedule_.empty()) { + // scheduler execute things out of order which may lead to incorrect state + // this->defer(std::move(to_schedule_.front())); + // let's execute it directly from the loop + fn = std::move(to_schedule_.front()); + to_schedule_.pop_front(); + } + xSemaphoreGive(this->to_schedule_lock_); + if (fn) { + fn(); + } + } +#endif + this->entities_iterator_.advance(); +} void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port()); @@ -413,13 +439,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM std::string data = this->switch_json(obj, obj->state, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { - this->defer([obj]() { obj->toggle(); }); + this->schedule_([obj]() { obj->toggle(); }); request->send(200); } else if (match.method == "turn_on") { - this->defer([obj]() { obj->turn_on(); }); + this->schedule_([obj]() { obj->turn_on(); }); request->send(200); } else if (match.method == "turn_off") { - this->defer([obj]() { obj->turn_off(); }); + this->schedule_([obj]() { obj->turn_off(); }); request->send(200); } else { request->send(404); @@ -441,7 +467,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_POST && match.method == "press") { - this->defer([obj]() { obj->press(); }); + this->schedule_([obj]() { obj->press(); }); request->send(200); return; } else { @@ -497,7 +523,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc std::string data = this->fan_json(obj, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { - this->defer([obj]() { obj->toggle().perform(); }); + this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); @@ -531,10 +557,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc return; } } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); } else if (match.method == "turn_off") { - this->defer([obj]() { obj->turn_off().perform(); }); + this->schedule_([obj]() { obj->turn_off().perform(); }); request->send(200); } else { request->send(404); @@ -558,7 +584,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa std::string data = this->light_json(obj, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { - this->defer([obj]() { obj->toggle().perform(); }); + this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); @@ -590,7 +616,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa call.set_effect(effect); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); } else if (match.method == "turn_off") { auto call = obj->turn_off(); @@ -598,7 +624,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; call.set_transition_length(length); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); } else { request->send(404); @@ -663,7 +689,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->hasParam("tilt")) call.set_tilt(request->getParam("tilt")->value().toFloat()); - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -708,7 +734,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM call.set_value(*value_f); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -765,7 +791,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -833,7 +859,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url call.set_target_temperature(*value_f); } - this->defer([call]() mutable { call.perform(); }); + this->schedule_([call]() mutable { call.perform(); }); request->send(200); return; } @@ -949,13 +975,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat std::string data = this->lock_json(obj, obj->state, DETAIL_STATE); request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { - this->defer([obj]() { obj->lock(); }); + this->schedule_([obj]() { obj->lock(); }); request->send(200); } else if (match.method == "unlock") { - this->defer([obj]() { obj->unlock(); }); + this->schedule_([obj]() { obj->unlock(); }); request->send(200); } else if (match.method == "open") { - this->defer([obj]() { obj->open(); }); + this->schedule_([obj]() { obj->open(); }); request->send(200); } else { request->send(404); @@ -1154,6 +1180,16 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { bool WebServer::isRequestHandlerTrivial() { return false; } +void WebServer::schedule_(std::function &&f) { +#ifdef USE_ESP32 + xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY); + to_schedule_.push_back(std::move(f)); + xSemaphoreGive(this->to_schedule_lock_); +#else + this->defer(std::move(f)); +#endif +} + } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 78d0597e61..f4122ef754 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -9,7 +9,11 @@ #include "esphome/core/controller.h" #include - +#ifdef USE_ESP32 +#include +#include +#include +#endif namespace esphome { namespace web_server { @@ -34,7 +38,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; */ class WebServer : public Controller, public Component, public AsyncWebHandler { public: - WebServer(web_server_base::WebServerBase *base) : base_(base), entities_iterator_(ListEntitiesIterator(this)) {} + WebServer(web_server_base::WebServerBase *base); /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css @@ -220,6 +224,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { bool isRequestHandlerTrivial() override; protected: + void schedule_(std::function &&f); friend ListEntitiesIterator; web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; @@ -230,6 +235,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { const char *js_include_{nullptr}; bool include_internal_{false}; bool allow_ota_{true}; +#ifdef USE_ESP32 + std::deque> to_schedule_; + SemaphoreHandle_t to_schedule_lock_; +#endif }; } // namespace web_server diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py new file mode 100644 index 0000000000..7b05c43198 --- /dev/null +++ b/esphome/components/wiegand/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.components import key_provider +from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID + +CODEOWNERS = ["@ssieb"] + +AUTO_LOAD = ["key_provider"] + +MULTI_CONF = True + +wiegand_ns = cg.esphome_ns.namespace("wiegand") + +Wiegand = wiegand_ns.class_("Wiegand", key_provider.KeyProvider, cg.Component) +WiegandTagTrigger = wiegand_ns.class_( + "WiegandTagTrigger", automation.Trigger.template(cg.std_string) +) +WiegandRawTrigger = wiegand_ns.class_( + "WiegandRawTrigger", automation.Trigger.template(cg.uint8, cg.uint64) +) +WiegandKeyTrigger = wiegand_ns.class_( + "WiegandKeyTrigger", automation.Trigger.template(cg.uint8) +) + +CONF_D0 = "d0" +CONF_D1 = "d1" +CONF_ON_KEY = "on_key" +CONF_ON_RAW = "on_raw" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Wiegand), + cv.Required(CONF_D0): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_D1): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandTagTrigger), + } + ), + cv.Optional(CONF_ON_RAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandRawTrigger), + } + ), + cv.Optional(CONF_ON_KEY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandKeyTrigger), + } + ), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + pin = await cg.gpio_pin_expression(config[CONF_D0]) + cg.add(var.set_d0_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_D1]) + cg.add(var.set_d1_pin(pin)) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_tag_trigger(trigger)) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_raw_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.uint8, "bits"), (cg.uint64, "value")], conf + ) + + for conf in config.get(CONF_ON_KEY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_key_trigger(trigger)) + await automation.build_automation(trigger, [(cg.uint8, "x")], conf) diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp new file mode 100644 index 0000000000..67558da731 --- /dev/null +++ b/esphome/components/wiegand/wiegand.cpp @@ -0,0 +1,117 @@ +#include "wiegand.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace wiegand { + +static const char *const TAG = "wiegand"; +static const char *const KEYS = "0123456789*#"; + +void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { + if (arg->d0.digital_read()) + return; + arg->count++; + arg->value <<= 1; + arg->last_bit_time = millis(); + arg->done = false; +} + +void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { + if (arg->d1.digital_read()) + return; + arg->count++; + arg->value = (arg->value << 1) | 1; + arg->last_bit_time = millis(); + arg->done = false; +} + +void Wiegand::setup() { + this->d0_pin_->setup(); + this->store_.d0 = this->d0_pin_->to_isr(); + this->d1_pin_->setup(); + this->store_.d1 = this->d1_pin_->to_isr(); + this->d0_pin_->attach_interrupt(WiegandStore::d0_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->d1_pin_->attach_interrupt(WiegandStore::d1_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); +} + +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) + parity++; + } + return !(parity & 1); +} + +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) + parity++; + } + return parity & 1; +} + +void Wiegand::loop() { + if (this->store_.done) + return; + if (millis() - this->store_.last_bit_time < 100) + return; + uint8_t count = this->store_.count; + uint64_t value = this->store_.value; + this->store_.count = 0; + this->store_.value = 0; + this->store_.done = true; + ESP_LOGV(TAG, "received %d-bit value: %llx", count, value); + for (auto *trigger : this->raw_triggers_) + trigger->trigger(count, value); + if (count == 26) { + std::string tag = to_string((value >> 1) & 0xffffff); + ESP_LOGD(TAG, "received 26-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 13, 13) || !check_oparity(value, 0, 13)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 34) { + std::string tag = to_string((value >> 1) & 0xffffffff); + ESP_LOGD(TAG, "received 34-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 17, 17) || !check_oparity(value, 0, 17)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 37) { + std::string tag = to_string((value >> 1) & 0x7ffffffff); + ESP_LOGD(TAG, "received 37-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 18, 19) || !check_oparity(value, 0, 19)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 4) { + for (auto *trigger : this->key_triggers_) + trigger->trigger(value); + if (value < 12) { + uint8_t key = KEYS[value]; + this->send_key_(key); + } + } else { + ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value); + } +} + +void Wiegand::dump_config() { + ESP_LOGCONFIG(TAG, "Wiegand reader:"); + LOG_PIN(" D0 pin: ", this->d0_pin_); + LOG_PIN(" D1 pin: ", this->d1_pin_); +} + +} // namespace wiegand +} // namespace esphome diff --git a/esphome/components/wiegand/wiegand.h b/esphome/components/wiegand/wiegand.h new file mode 100644 index 0000000000..994631a3a3 --- /dev/null +++ b/esphome/components/wiegand/wiegand.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace wiegand { + +class Wiegand; + +struct WiegandStore { + ISRInternalGPIOPin d0; + ISRInternalGPIOPin d1; + volatile uint64_t value{0}; + volatile uint32_t last_bit_time{0}; + volatile bool done{true}; + volatile uint8_t count{0}; + + static void d0_gpio_intr(WiegandStore *arg); + static void d1_gpio_intr(WiegandStore *arg); +}; + +class WiegandTagTrigger : public Trigger {}; + +class WiegandRawTrigger : public Trigger {}; + +class WiegandKeyTrigger : public Trigger {}; + +class Wiegand : public key_provider::KeyProvider, public Component { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + + void set_d0_pin(InternalGPIOPin *pin) { this->d0_pin_ = pin; }; + void set_d1_pin(InternalGPIOPin *pin) { this->d1_pin_ = pin; }; + void register_tag_trigger(WiegandTagTrigger *trig) { this->tag_triggers_.push_back(trig); } + void register_raw_trigger(WiegandRawTrigger *trig) { this->raw_triggers_.push_back(trig); } + void register_key_trigger(WiegandKeyTrigger *trig) { this->key_triggers_.push_back(trig); } + + protected: + InternalGPIOPin *d0_pin_; + InternalGPIOPin *d1_pin_; + WiegandStore store_{}; + std::vector tag_triggers_; + std::vector raw_triggers_; + std::vector key_triggers_; +}; + +} // namespace wiegand +} // namespace esphome diff --git a/esphome/components/x9c/__init__.py b/esphome/components/x9c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/x9c/output.py b/esphome/components/x9c/output.py new file mode 100644 index 0000000000..44e9d729b3 --- /dev/null +++ b/esphome/components/x9c/output.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import output +from esphome.const import ( + CONF_ID, + CONF_CS_PIN, + CONF_INC_PIN, + CONF_UD_PIN, + CONF_INITIAL_VALUE, +) + +CODEOWNERS = ["@EtienneMD"] + +x9c_ns = cg.esphome_ns.namespace("x9c") + +X9cOutput = x9c_ns.class_("X9cOutput", output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(X9cOutput), + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_INC_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_UD_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_INITIAL_VALUE, default=1.0): cv.float_range( + min=0.01, max=1.0 + ), + } + ) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + cs_pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(cs_pin)) + inc_pin = await cg.gpio_pin_expression(config[CONF_INC_PIN]) + cg.add(var.set_inc_pin(inc_pin)) + ud_pin = await cg.gpio_pin_expression(config[CONF_UD_PIN]) + cg.add(var.set_ud_pin(ud_pin)) + + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp new file mode 100644 index 0000000000..ff7777e71f --- /dev/null +++ b/esphome/components/x9c/x9c.cpp @@ -0,0 +1,72 @@ +#include "x9c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace x9c { + +static const char *const TAG = "x9c.output"; + +void X9cOutput::trim_value(int change_amount) { + if (change_amount > 0) { // Set change direction + this->ud_pin_->digital_write(true); + } else { + this->ud_pin_->digital_write(false); + } + + this->inc_pin_->digital_write(true); + this->cs_pin_->digital_write(false); // Select chip + + for (int i = 0; i < abs(change_amount); i++) { // Move wiper + this->inc_pin_->digital_write(true); + delayMicroseconds(1); + this->inc_pin_->digital_write(false); + delayMicroseconds(1); + } + + delayMicroseconds(100); // Let value settle + + this->inc_pin_->digital_write(false); + this->cs_pin_->digital_write(true); // Deselect chip safely (no save) +} + +void X9cOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up X9C Potentiometer with initial value of %f", this->initial_value_); + + this->inc_pin_->get_pin(); + this->inc_pin_->setup(); + this->inc_pin_->digital_write(false); + + this->cs_pin_->get_pin(); + this->cs_pin_->setup(); + this->cs_pin_->digital_write(true); + + this->ud_pin_->get_pin(); + this->ud_pin_->setup(); + + if (this->initial_value_ <= 0.50) { + this->trim_value(-101); // Set min value (beyond 0) + this->trim_value((int) (this->initial_value_ * 100)); + } else { + this->trim_value(101); // Set max value (beyond 100) + this->trim_value((int) (this->initial_value_ * 100) - 100); + } + this->pot_value_ = this->initial_value_; + this->write_state(this->initial_value_); +} + +void X9cOutput::write_state(float state) { + this->trim_value((int) ((state - this->pot_value_) * 100)); + this->pot_value_ = state; +} + +void X9cOutput::dump_config() { + ESP_LOGCONFIG(TAG, "X9C Potentiometer Output:"); + LOG_PIN(" Chip Select Pin: ", this->cs_pin_); + LOG_PIN(" Increment Pin: ", this->inc_pin_); + LOG_PIN(" Up/Down Pin: ", this->ud_pin_); + ESP_LOGCONFIG(TAG, " Initial Value: %f", this->initial_value_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace x9c +} // namespace esphome diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h new file mode 100644 index 0000000000..924460c841 --- /dev/null +++ b/esphome/components/x9c/x9c.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace x9c { + +class X9cOutput : public output::FloatOutput, public Component { + public: + void set_cs_pin(InternalGPIOPin *pin) { cs_pin_ = pin; } + void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; } + void set_ud_pin(InternalGPIOPin *pin) { ud_pin_ = pin; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + + void setup() override; + void dump_config() override; + + void trim_value(int change_amount); + + protected: + void write_state(float state) override; + InternalGPIOPin *cs_pin_; + InternalGPIOPin *inc_pin_; + InternalGPIOPin *ud_pin_; + float initial_value_; + float pot_value_; +}; + +} // namespace x9c +} // namespace esphome diff --git a/esphome/config.py b/esphome/config.py index e63cddf347..b04de020e0 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -14,6 +14,7 @@ from esphome import core, yaml_util, loader import esphome.core.config as core_config from esphome.const import ( CONF_ESPHOME, + CONF_ID, CONF_PLATFORM, CONF_PACKAGES, CONF_SUBSTITUTIONS, @@ -24,6 +25,7 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict +from esphome.config_helpers import Extend from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid @@ -334,6 +336,13 @@ class LoadValidationStep(ConfigValidationStep): continue p_name = p_config.get("platform") if p_name is None: + p_id = p_config.get(CONF_ID) + if isinstance(p_id, Extend): + result.add_str_error( + f"Source for extension of ID '{p_id.value}' was not found.", + path + [CONF_ID], + ) + continue result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index d36a2f1e7f..e1d63775bb 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,10 +1,27 @@ import json import os +from esphome.const import CONF_ID from esphome.core import CORE from esphome.helpers import read_file +class Extend: + def __init__(self, value): + self.value = value + + def __str__(self): + return f"!extend {self.value}" + + def __eq__(self, b): + """ + Check if two Extend objects contain the same ID. + + Only used in unit tests. + """ + return isinstance(b, Extend) and self.value == b.value + + def read_config_file(path: str) -> str: if CORE.vscode and ( not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) @@ -36,7 +53,25 @@ def merge_config(full_old, full_new): if isinstance(new, list): if not isinstance(old, list): return new - return old + new + res = old.copy() + ids = { + v[CONF_ID]: i + for i, v in enumerate(res) + if CONF_ID in v and isinstance(v[CONF_ID], str) + } + for v in new: + if CONF_ID in v: + new_id = v[CONF_ID] + if isinstance(new_id, Extend): + new_id = new_id.value + if new_id in ids: + v[CONF_ID] = new_id + res[ids[new_id]] = merge(res[ids[new_id]], v) + continue + else: + ids[new_id] = len(res) + res.append(v) + return res if new is None: return old diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 054ee194c1..7440f71790 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -13,6 +13,7 @@ import voluptuous as vol from esphome import core import esphome.codegen as cg +from esphome.config_helpers import Extend from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, @@ -490,6 +491,8 @@ def declare_id(type): if value is None: return core.ID(None, is_declaration=True, type=type) + if isinstance(value, Extend): + raise Invalid(f"Source for extension of ID '{value.value}' was not found.") return core.ID(validate_id_name(value), is_declaration=True, type=type) return validator @@ -1240,7 +1243,7 @@ def enum(mapping, **kwargs): return validator -LAMBDA_ENTITY_ID_PROG = re.compile(r"id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)") +LAMBDA_ENTITY_ID_PROG = re.compile(r"\Wid\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)") def lambda_(value): diff --git a/esphome/const.py b/esphome/const.py index 163ddc75b2..ce3eac9ed6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.12.8" +__version__ = "2023.2.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -12,7 +12,7 @@ TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} -SECRETS_FILES = {"secrets.yaml", "secrets.yml"} +SECRETS_FILES = ("secrets.yaml", "secrets.yml") CONF_ABOVE = "above" @@ -26,6 +26,7 @@ CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" +CONF_ACTUAL_GAIN = "actual_gain" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" @@ -140,6 +141,7 @@ CONF_CURRENT = "current" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" +CONF_CUSTOM = "custom" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" CONF_CUSTOM_FAN_MODES = "custom_fan_modes" CONF_CUSTOM_PRESET = "custom_preset" @@ -166,6 +168,7 @@ CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" CONF_DELAY = "delay" CONF_DELIMITER = "delimiter" CONF_DELTA = "delta" +CONF_DEST = "dest" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" @@ -175,6 +178,7 @@ CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" +CONF_DISABLE_CRC = "disable_crc" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCONNECT_DELAY = "disconnect_delay" CONF_DISCOVERY = "discovery" @@ -217,6 +221,7 @@ CONF_EVENT = "event" CONF_EXPIRE_AFTER = "expire_after" CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy" CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy" +CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input" CONF_EXTERNAL_COMPONENTS = "external_components" CONF_EXTERNAL_VCC = "external_vcc" CONF_FALLING_EDGE = "falling_edge" @@ -232,6 +237,7 @@ CONF_FAN_MODE_MEDIUM_ACTION = "fan_mode_medium_action" CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action" CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action" CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action" +CONF_FAN_MODE_QUIET_ACTION = "fan_mode_quiet_action" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" CONF_FAN_ONLY_ACTION = "fan_only_action" CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER = "fan_only_action_uses_fan_mode_timer" @@ -259,6 +265,7 @@ CONF_FRAGMENTATION = "fragmentation" CONF_FRAMEWORK = "framework" CONF_FREE = "free" CONF_FREQUENCY = "frequency" +CONF_FRIENDLY_NAME = "friendly_name" CONF_FROM = "from" CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_UPDATE_EVERY = "full_update_every" @@ -306,6 +313,7 @@ CONF_ILLUMINANCE = "illuminance" CONF_IMPEDANCE = "impedance" CONF_IMPORT_ACTIVE_ENERGY = "import_active_energy" CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" +CONF_INC_PIN = "inc_pin" CONF_INCLUDE_INTERNAL = "include_internal" CONF_INCLUDES = "includes" CONF_INDEX = "index" @@ -462,6 +470,7 @@ CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TIME = "on_time" CONF_ON_TIME_SYNC = "on_time_sync" +CONF_ON_TIMEOUT = "on_timeout" CONF_ON_TOUCH = "on_touch" CONF_ON_TURN_OFF = "on_turn_off" CONF_ON_TURN_ON = "on_turn_on" @@ -542,8 +551,10 @@ CONF_POWER_SAVE_MODE = "power_save_mode" CONF_POWER_SUPPLY = "power_supply" CONF_PRESET = "preset" CONF_PRESET_BOOST = "preset_boost" +CONF_PRESET_COMMAND_TOPIC = "preset_command_topic" CONF_PRESET_ECO = "preset_eco" CONF_PRESET_SLEEP = "preset_sleep" +CONF_PRESET_STATE_TOPIC = "preset_state_topic" CONF_PRESSURE = "pressure" CONF_PRIORITY = "priority" CONF_PROJECT = "project" @@ -741,6 +752,7 @@ CONF_TX_POWER = "tx_power" CONF_TYPE = "type" CONF_TYPE_ID = "type_id" CONF_UART_ID = "uart_id" +CONF_UD_PIN = "ud_pin" CONF_UID = "uid" CONF_UNIQUE = "unique" CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" @@ -855,6 +867,7 @@ UNIT_AMPERE = "A" UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³" UNIT_BYTES = "B" UNIT_CELSIUS = "°C" +UNIT_CENTIMETER = "cm" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" @@ -866,6 +879,7 @@ UNIT_EMPTY = "" UNIT_G = "G" UNIT_HECTOPASCAL = "hPa" UNIT_HERTZ = "Hz" +UNIT_HOUR = "h" UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" @@ -953,6 +967,7 @@ DEVICE_CLASS_PM1 = "pm1" DEVICE_CLASS_PM10 = "pm10" DEVICE_CLASS_PM25 = "pm25" DEVICE_CLASS_POWER_FACTOR = "power_factor" +DEVICE_CLASS_PRECIPITATION = "precipitation" DEVICE_CLASS_PRECIPITATION_INTENSITY = "precipitation_intensity" DEVICE_CLASS_PRESSURE = "pressure" DEVICE_CLASS_REACTIVE_POWER = "reactive_power" @@ -993,6 +1008,7 @@ KEY_TARGET_PLATFORM = "target_platform" KEY_TARGET_FRAMEWORK = "target_framework" KEY_FRAMEWORK_VERSION = "framework_version" KEY_NAME = "name" +KEY_VARIANT = "variant" # Entity categories ENTITY_CATEGORY_NONE = "" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 09115c2791..545fae381f 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -453,6 +453,8 @@ class EsphomeCore: self.ace = False # The name of the node self.name: Optional[str] = None + # The friendly name of the node + self.friendly_name: Optional[str] = None # Additional data components can store temporary data in # The first key to this dict should always be the integration name self.data = {} @@ -492,6 +494,7 @@ class EsphomeCore: def reset(self): self.dashboard = False self.name = None + self.friendly_name = None self.data = {} self.config_path = None self.build_path = None diff --git a/esphome/core/application.h b/esphome/core/application.h index 6376987f66..0992a4df39 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -2,11 +2,11 @@ #include #include -#include "esphome/core/defines.h" -#include "esphome/core/preferences.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" #ifdef USE_BINARY_SENSOR @@ -53,14 +53,22 @@ namespace esphome { class Application { public: - void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &comment, + const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { this->name_ = name + "-" + get_mac_address().substr(6); + if (friendly_name.empty()) { + this->friendly_name_ = ""; + } else { + this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6); + } } else { this->name_ = name; + this->friendly_name_ = friendly_name; } + this->comment_ = comment; this->compilation_time_ = compilation_time; } @@ -131,9 +139,14 @@ class Application { /// Make a loop iteration. Call this in your loop() function. void loop(); - /// Get the name of this Application set by set_name(). + /// Get the name of this Application set by pre_setup(). const std::string &get_name() const { return this->name_; } + /// Get the friendly name of this Application set by pre_setup(). + const std::string &get_friendly_name() const { return this->friendly_name_; } + /// Get the comment of this Application set by pre_setup(). + const std::string &get_comment() const { return this->comment_; } + bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } const std::string &get_compilation_time() const { return this->compilation_time_; } @@ -338,6 +351,8 @@ class Application { #endif std::string name_; + std::string friendly_name_; + std::string comment_; std::string compilation_time_; bool name_add_mac_suffix_; uint32_t last_loop_{0}; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 7505e1f39b..b1ace8b2d1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -57,7 +57,7 @@ bool Component::cancel_interval(const std::string &name) { // NOLINT } void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&f, float backoff_increase_factor) { // NOLINT + std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } @@ -130,7 +130,7 @@ void Component::set_timeout(uint32_t timeout, std::function &&f) { // N void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT App.scheduler.set_interval(this, "", interval, std::move(f)); } -void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, +void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 1d8499e262..769e74e645 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -185,26 +185,39 @@ class Component { /** Set an retry function with a unique name. Empty name means no cancelling possible. * - * This will call f. If f returns RetryResult::RETRY f is called again after initial_wait_time ms. - * f should return RetryResult::DONE if no repeat is required. The initial wait time will be increased - * by backoff_increase_factor for each iteration. Default is doubling the time between iterations - * Can be cancelled via cancel_retry(). + * This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if + * it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f + * again in the future. + * + * The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is + * increased by multipling by `backoff_increase_factor` each time. If no backoff_increase_factor is + * supplied (default = 1.0), the wait time will stay constant. + * + * The retry function f needs to accept a single argument: the number of attempts remaining. On the + * final retry of f, this value will be 0. + * + * This retry function can also be cancelled by name via cancel_retry(). * * IMPORTANT: Do not rely on this having correct timing. This is only called from * loop() and therefore can be significantly delayed. * + * REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead. + * + * REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly + * if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows. + * * @param name The identifier for this retry function. * @param initial_wait_time The time in ms before f is called again - * @param max_attempts The maximum number of retries + * @param max_attempts The maximum number of executions * @param f The function (or lambda) that should be called - * @param backoff_increase_factor time between retries is increased by this factor on every retry + * @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first * @see cancel_retry() */ - void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT - std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT + void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT + std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT - void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT - float backoff_increase_factor = 1.0f); // NOLINT + void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT + float backoff_increase_factor = 1.0f); // NOLINT /** Cancel a retry function. * diff --git a/esphome/core/config.py b/esphome/core/config.py index bf0beea2ef..ef6553026e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -19,6 +19,7 @@ from esphome.const import ( CONF_LIBRARIES, CONF_MIN_VERSION, CONF_NAME, + CONF_FRIENDLY_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, @@ -124,6 +125,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -192,6 +194,7 @@ def preload_core_config(config, result): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) CORE.name = conf[CONF_NAME] + CORE.friendly_name = conf.get(CONF_FRIENDLY_NAME) CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: @@ -346,6 +349,8 @@ async def to_code(config): cg.add( cg.App.pre_setup( config[CONF_NAME], + config[CONF_FRIENDLY_NAME], + config.get(CONF_COMMENT, ""), cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], ) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index cc4074b94d..d880f0fda4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -74,7 +74,7 @@ bool HOT Scheduler::cancel_interval(Component *component, const std::string &nam } struct RetryArgs { - std::function func; + std::function func; uint8_t retry_countdown; uint32_t current_interval; Component *component; @@ -84,15 +84,18 @@ struct RetryArgs { }; static void retry_handler(const std::shared_ptr &args) { - RetryResult retry_result = args->func(); - if (retry_result == RetryResult::DONE || --args->retry_countdown <= 0) + RetryResult const retry_result = args->func(--args->retry_countdown); + if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) return; - args->current_interval *= args->backoff_increase_factor; + // second execution of `func` happens after `initial_wait_time` args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); }); + // backoff_increase_factor applied to third & later executions + args->current_interval *= args->backoff_increase_factor; } void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, - uint8_t max_attempts, std::function func, float backoff_increase_factor) { + uint8_t max_attempts, std::function func, + float backoff_increase_factor) { if (!name.empty()) this->cancel_retry(component, name); @@ -102,6 +105,13 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); + if (backoff_increase_factor < 0.0001) { + ESP_LOGE(TAG, + "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead", + name.c_str(), backoff_increase_factor); + backoff_increase_factor = 1; + } + auto args = std::make_shared(); args->func = std::move(func); args->retry_countdown = max_attempts; @@ -111,7 +121,8 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin args->backoff_increase_factor = backoff_increase_factor; args->scheduler = this; - this->set_timeout(component, args->name, initial_wait_time, [args]() { retry_handler(args); }); + // First exectuion 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) { return this->cancel_timeout(component, "retry$" + name); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 111bee1df2..a758198b8d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -16,7 +16,7 @@ class Scheduler { bool cancel_interval(Component *component, const std::string &name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function func, float backoff_increase_factor = 1.0f); + std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); optional next_schedule_in(); diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 2cf826dea9..f7d471586d 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -25,10 +25,10 @@ import tornado.netutil import tornado.process import tornado.web import tornado.websocket +import yaml from tornado.log import access_log from esphome import const, platformio_api, util, yaml_util -from esphome.core import EsphomeError from esphome.helpers import get_bool_env, mkdir_p, run_system_command from esphome.storage_json import ( EsphomeStorageJSON, @@ -40,7 +40,7 @@ from esphome.storage_json import ( from esphome.util import get_serial_ports, shlex_quote from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf -from .util import password_hash +from .util import friendly_name_slugify, password_hash _LOGGER = logging.getLogger(__name__) @@ -55,6 +55,7 @@ class DashboardSettings: self.using_password = False self.on_ha_addon = False self.cookie_secret = None + self.absolute_config_dir = None def parse_args(self, args): self.on_ha_addon = args.ha_addon @@ -65,6 +66,7 @@ class DashboardSettings: if self.using_password: self.password_hash = password_hash(password) self.config_dir = args.configuration + self.absolute_config_dir = Path(self.config_dir).resolve() @property def relative_url(self): @@ -94,7 +96,10 @@ class DashboardSettings: return hmac.compare_digest(self.password_hash, password_hash(password)) def rel_path(self, *args): - return os.path.join(self.config_dir, *args) + joined_path = os.path.join(self.config_dir, *args) + # Raises ValueError if not relative to ESPHome config folder + Path(joined_path).resolve().relative_to(self.absolute_config_dir) + return joined_path def list_yaml_files(self): return util.list_yaml_files([self.config_dir]) @@ -281,8 +286,11 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket): class EsphomeRenameHandler(EsphomeCommandWebSocket): + old_name: str + def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) + self.old_name = json_message["configuration"] return [ "esphome", "--dashboard", @@ -291,8 +299,30 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket): json_message["newName"], ] + def _proc_on_exit(self, returncode): + super()._proc_on_exit(returncode) + + if returncode != 0: + return + + # Remove the old ping result from the cache + PING_RESULT.pop(self.old_name, None) + class EsphomeUploadHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + config_file = settings.rel_path(json_message["configuration"]) + return [ + "esphome", + "--dashboard", + "upload", + config_file, + "--device", + json_message["port"], + ] + + +class EsphomeRunHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) return [ @@ -378,12 +408,24 @@ class WizardRequestHandler(BaseHandler): for k, v in json.loads(self.request.body.decode()).items() if k in ("name", "platform", "board", "ssid", "psk", "password") } + if not kwargs["name"]: + self.set_status(422) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Name is required"})) + return + + kwargs["friendly_name"] = kwargs["name"] + kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) + kwargs["ota_password"] = secrets.token_hex(16) noise_psk = secrets.token_bytes(32) kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() - destination = settings.rel_path(f"{kwargs['name']}.yaml") + filename = f"{kwargs['name']}.yaml" + destination = settings.rel_path(filename) wizard.wizard_write(path=destination, **kwargs) self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) self.finish() @@ -395,6 +437,8 @@ class ImportRequestHandler(BaseHandler): args = json.loads(self.request.body.decode()) try: name = args["name"] + friendly_name = args.get("friendly_name") + encryption = args.get("encryption", False) imported_device = next( (res for res in IMPORT_RESULT.values() if res.device_name == name), None @@ -402,15 +446,19 @@ class ImportRequestHandler(BaseHandler): if imported_device is not None: network = imported_device.network + if friendly_name is None: + friendly_name = imported_device.friendly_name else: network = const.CONF_WIFI import_config( settings.rel_path(f"{name}.yaml"), name, + friendly_name, args["project_name"], args["package_import_url"], network, + encryption, ) except FileExistsError: self.set_status(500) @@ -422,6 +470,8 @@ class ImportRequestHandler(BaseHandler): return self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": f"{name}.yaml"})) self.finish() @@ -495,35 +545,11 @@ class DownloadBinaryRequestHandler(BaseHandler): self.finish() -class ManifestRequestHandler(BaseHandler): +class EsphomeVersionHandler(BaseHandler): @authenticated - @bind_config - def get(self, configuration=None): - args = ["esphome", "idedata", settings.rel_path(configuration)] - rc, stdout, _ = run_system_command(*args) - - if rc != 0: - self.send_error(404 if rc == 2 else 500) - return - - idedata = platformio_api.IDEData(json.loads(stdout)) - - firmware_offset = "0x10000" if idedata.extra_flash_images else "0x0" - flash_images = [ - { - "path": f"./download.bin?configuration={configuration}&type=firmware.bin", - "offset": firmware_offset, - } - ] + [ - { - "path": f"./download.bin?configuration={configuration}&type={os.path.basename(image.path)}", - "offset": image.offset, - } - for image in idedata.extra_flash_images - ] - + def get(self): self.set_header("Content-Type", "application/json") - self.write(json.dumps(flash_images)) + self.write(json.dumps({"version": const.__version__})) self.finish() @@ -569,6 +595,12 @@ class DashboardEntry: return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name + @property + def friendly_name(self): + if self.storage is None: + return self.name + return self.storage.friendly_name + @property def comment(self): if self.storage is None: @@ -616,6 +648,7 @@ class ListDevicesHandler(BaseHandler): "configured": [ { "name": entry.name, + "friendly_name": entry.friendly_name, "configuration": entry.filename, "loaded_integrations": entry.loaded_integrations, "deployed_version": entry.update_old, @@ -631,6 +664,7 @@ class ListDevicesHandler(BaseHandler): "importable": [ { "name": res.device_name, + "friendly_name": res.friendly_name, "package_import_url": res.package_import_url, "project_name": res.project_name, "project_version": res.project_version, @@ -695,20 +729,34 @@ class PrometheusServiceDiscoveryHandler(BaseHandler): class BoardsRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self, platform: str): from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS - boards = { - "esp32": {key: val[const.KEY_NAME] for key, val in ESP32_BOARDS.items()}, - "esp8266": { - key: val[const.KEY_NAME] for key, val in ESP8266_BOARDS.items() - }, - "rp2040": {key: val[const.KEY_NAME] for key, val in RP2040_BOARDS.items()}, + platform_to_boards = { + "esp32": ESP32_BOARDS, + "esp8266": ESP8266_BOARDS, + "rp2040": RP2040_BOARDS, } + # filter all ESP32 variants by requested platform + if platform.startswith("esp32"): + boards = { + k: v + for k, v in platform_to_boards["esp32"].items() + if v[const.KEY_VARIANT] == platform.upper() + } + else: + boards = platform_to_boards[platform] + + # map to a {board_name: board_title} dict + 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))] + self.set_header("content-type", "application/json") - self.write(json.dumps(boards)) + self.write(json.dumps(output)) class MDNSStatusThread(threading.Thread): @@ -845,6 +893,9 @@ class DeleteRequestHandler(BaseHandler): if build_folder is not None: shutil.rmtree(build_folder, os.path.join(trash_path, name)) + # Remove the old ping result from the cache + PING_RESULT.pop(configuration, None) + class UndoDeleteRequestHandler(BaseHandler): @authenticated @@ -953,6 +1004,20 @@ class SecretKeysRequestHandler(BaseHandler): self.write(json.dumps(secret_keys)) +class SafeLoaderIgnoreUnknown(yaml.SafeLoader): + def ignore_unknown(self, node): + return f"{node.tag} {node.value}" + + def construct_yaml_binary(self, node) -> str: + return super().construct_yaml_binary(node).decode("ascii") + + +SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) +SafeLoaderIgnoreUnknown.add_constructor( + "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary +) + + class JsonConfigRequestHandler(BaseHandler): @authenticated @bind_config @@ -962,16 +1027,18 @@ class JsonConfigRequestHandler(BaseHandler): self.send_error(404) return - try: - content = yaml_util.load_yaml(filename, clear_secrets=False) - json_content = json.dumps( - content, default=lambda o: {"__type": str(type(o)), "repr": repr(o)} - ) - self.set_header("content-type", "application/json") - self.write(json_content) - except EsphomeError as err: - _LOGGER.warning("Error translating file %s to JSON: %s", filename, err) - self.send_error(500) + args = ["esphome", "config", settings.rel_path(configuration), "--show-secrets"] + + rc, stdout, _ = run_system_command(*args) + + if rc != 0: + self.send_error(422) + return + + data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) + self.set_header("content-type", "application/json") + self.write(json.dumps(data)) + self.finish() def get_base_frontend_path(): @@ -1058,6 +1125,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}logout", LogoutHandler), (f"{rel}logs", EsphomeLogsHandler), (f"{rel}upload", EsphomeUploadHandler), + (f"{rel}run", EsphomeRunHandler), (f"{rel}compile", EsphomeCompileHandler), (f"{rel}validate", EsphomeValidateHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), @@ -1068,7 +1136,6 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}info", InfoRequestHandler), (f"{rel}edit", EditRequestHandler), (f"{rel}download.bin", DownloadBinaryRequestHandler), - (f"{rel}manifest.json", ManifestRequestHandler), (f"{rel}serial-ports", SerialPortRequestHandler), (f"{rel}ping", PingRequestHandler), (f"{rel}delete", DeleteRequestHandler), @@ -1081,7 +1148,8 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}json-config", JsonConfigRequestHandler), (f"{rel}rename", EsphomeRenameHandler), (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), - (f"{rel}boards", BoardsRequestHandler), + (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), + (f"{rel}version", EsphomeVersionHandler), ], **app_settings, ) diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py index 3e3864aa17..a2ad530b74 100644 --- a/esphome/dashboard/util.py +++ b/esphome/dashboard/util.py @@ -1,4 +1,7 @@ import hashlib +import unicodedata + +from esphome.const import ALLOWED_NAME_CHARS def password_hash(password: str) -> bytes: @@ -7,3 +10,23 @@ def password_hash(password: str) -> bytes: Note this is not meant for secure storage, but for securely comparing passwords. """ return hashlib.sha256(password.encode()).digest() + + +def strip_accents(value): + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def friendly_name_slugify(value): + value = ( + strip_accents(value) + .lower() + .replace(" ", "-") + .replace("_", "-") + .replace("--", "-") + .strip("-") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/pins.py b/esphome/pins.py index 2b3adce86d..2ac4cd4b54 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -49,6 +49,11 @@ def _set_mode(value, default_mode): CONF_INPUT: True, CONF_PULLDOWN: True, }, + "INPUT_OUTPUT_OPEN_DRAIN": { + CONF_INPUT: True, + CONF_OUTPUT: True, + CONF_OPEN_DRAIN: True, + }, } if mode.upper() not in PIN_MODES: raise cv.Invalid(f"Unknown pin mode {mode}", [CONF_MODE]) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index c2c8b91a36..bbdfbbc8a2 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -36,6 +36,7 @@ class StorageJSON: self, storage_version, name, + friendly_name, comment, esphome_version, src_version, @@ -51,6 +52,8 @@ class StorageJSON: self.storage_version: int = storage_version # The name of the node self.name: str = name + # The friendly name of the node + self.friendly_name: str = friendly_name # The comment of the node self.comment: str = comment # The esphome version this was compiled with @@ -77,6 +80,7 @@ class StorageJSON: return { "storage_version": self.storage_version, "name": self.name, + "friendly_name": self.friendly_name, "comment": self.comment, "esphome_version": self.esphome_version, "src_version": self.src_version, @@ -106,6 +110,7 @@ class StorageJSON: return StorageJSON( storage_version=1, name=esph.name, + friendly_name=esph.friendly_name, comment=esph.comment, esphome_version=const.__version__, src_version=1, @@ -118,10 +123,13 @@ class StorageJSON: ) @staticmethod - def from_wizard(name: str, address: str, platform: str) -> "StorageJSON": + def from_wizard( + name: str, friendly_name: str, address: str, platform: str + ) -> "StorageJSON": return StorageJSON( storage_version=1, name=name, + friendly_name=friendly_name, comment=None, esphome_version=None, src_version=1, @@ -139,6 +147,7 @@ class StorageJSON: storage = json.load(f_handle) storage_version = storage["storage_version"] name = storage.get("name") + friendly_name = storage.get("friendly_name") comment = storage.get("comment") esphome_version = storage.get( "esphome_version", storage.get("esphomeyaml_version") @@ -153,6 +162,7 @@ class StorageJSON: return StorageJSON( storage_version, name, + friendly_name, comment, esphome_version, src_version, diff --git a/esphome/wizard.py b/esphome/wizard.py index 0fcccfc3f6..fd661af639 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -46,6 +46,11 @@ BASE_CONFIG = """esphome: name: {name} """ +BASE_CONFIG_FRIENDLY = """esphome: + name: {name} + friendly_name: {friendly_name} +""" + LOGGER_API_CONFIG = """ # Enable logging logger: @@ -110,7 +115,12 @@ def wizard_file(**kwargs): kwargs["fallback_name"] = ap_name kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) - config = BASE_CONFIG.format(**kwargs) + if kwargs.get("friendly_name"): + base = BASE_CONFIG_FRIENDLY + else: + base = BASE_CONFIG + + config = base.format(**kwargs) config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) @@ -192,7 +202,7 @@ def wizard_write(path, **kwargs): hardware = kwargs["platform"] write_file(path, wizard_file(**kwargs)) - storage = StorageJSON.from_wizard(name, f"{name}.local", hardware) + storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 2689ce9245..8a03c431a7 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -10,7 +10,7 @@ import yaml import yaml.constructor from esphome import core -from esphome.config_helpers import read_config_file +from esphome.config_helpers import read_config_file, Extend from esphome.core import ( EsphomeError, IPAddress, @@ -338,6 +338,10 @@ class ESPHomeLoader(yaml.SafeLoader): obj = self.construct_scalar(node) return add_class_to_obj(obj, ESPForceValue) + @_add_data_ref + def construct_extend(self, node): + return Extend(str(node.value)) + ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor( @@ -369,6 +373,7 @@ ESPHomeLoader.add_constructor( ) ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) +ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend) def load_yaml(fname, clear_secrets=True): @@ -390,8 +395,11 @@ def _load_yaml_internal(fname): loader.dispose() -def dump(dict_): +def dump(dict_, show_secrets=False): """Dump YAML to a string and remove null.""" + if show_secrets: + _SECRET_VALUES.clear() + _SECRET_CACHE.clear() return yaml.dump( dict_, default_flow_style=False, allow_unicode=True, Dumper=ESPHomeDumper ) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 3743f650f3..b0dddfd152 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -119,10 +119,12 @@ TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url" TXT_RECORD_PROJECT_NAME = b"project_name" TXT_RECORD_PROJECT_VERSION = b"project_version" TXT_RECORD_NETWORK = b"network" +TXT_RECORD_FRIENDLY_NAME = b"friendly_name" @dataclass class DiscoveredImport: + friendly_name: Optional[str] device_name: str package_import_url: str project_name: str @@ -155,6 +157,11 @@ class DashboardImportDiscovery: return if state_change == ServiceStateChange.Removed: self.import_state.pop(name, None) + return + + if state_change == ServiceStateChange.Updated and name not in self.import_state: + # Ignore updates for devices that are not in the import state + return info = zeroconf.get_service_info(service_type, name) _LOGGER.debug("-> resolved info: %s", info) @@ -174,8 +181,12 @@ class DashboardImportDiscovery: project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode() project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode() network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode() + friendly_name = info.properties.get(TXT_RECORD_FRIENDLY_NAME) + if friendly_name is not None: + friendly_name = friendly_name.decode() self.import_state[name] = DiscoveredImport( + friendly_name=friendly_name, device_name=node_name, package_import_url=import_url, project_name=project_name, diff --git a/requirements.txt b/requirements.txt index 9c12253160..9f4d34528c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,9 +9,9 @@ pyserial==3.5 platformio==6.1.5 # When updating platformio, also update Dockerfile esptool==4.4 click==8.1.3 -esphome-dashboard==20221213.0 -aioesphomeapi==13.0.1 -zeroconf==0.39.4 +esphome-dashboard==20230214.0 +aioesphomeapi==13.1.0 +zeroconf==0.47.1 # 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 fbc8f63613..8404818c95 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pylint==2.15.8 +pylint==2.15.10 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating -black==22.10.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 pre-commit # Unit tests -pytest==7.2.0 +pytest==7.2.1 pytest-cov==4.0.0 pytest-mock==3.10.0 -pytest-asyncio==0.20.2 +pytest-asyncio==0.20.3 asyncmock==0.4.2 hypothesis==5.49.0 diff --git a/tests/README.md b/tests/README.md index ed78b3e7d1..3238acaa79 100644 --- a/tests/README.md +++ b/tests/README.md @@ -25,3 +25,4 @@ Current test_.yaml file contents. | test4.yaml | ESP32 | ethernet | None | test5.yaml | ESP32 | wifi | ble_server | test6.yaml | RP2040 | wifi | N/A +| test7.yaml | ESP32-C3 | wifi | N/A diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py new file mode 100644 index 0000000000..0e24d78f5c --- /dev/null +++ b/tests/component_tests/packages/test_packages.py @@ -0,0 +1,351 @@ +"""Tests for the packages component.""" + +import pytest + + +from esphome.const import ( + CONF_DOMAIN, + CONF_ESPHOME, + CONF_FILTERS, + CONF_ID, + CONF_MULTIPLY, + CONF_NAME, + CONF_OFFSET, + CONF_PACKAGES, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_SENSOR, + CONF_SSID, + CONF_UPDATE_INTERVAL, + CONF_WIFI, +) +from esphome.components.packages import do_packages_pass +from esphome.config_helpers import Extend +import esphome.config_validation as cv + +# Test strings +TEST_DEVICE_NAME = "test_device_name" +TEST_PLATFORM = "test_platform" +TEST_WIFI_SSID = "test_wifi_ssid" +TEST_PACKAGE_WIFI_SSID = "test_package_wifi_ssid" +TEST_PACKAGE_WIFI_PASSWORD = "test_package_wifi_password" +TEST_DOMAIN = "test_domain_name" +TEST_SENSOR_PLATFORM_1 = "test_sensor_platform_1" +TEST_SENSOR_PLATFORM_2 = "test_sensor_platform_2" +TEST_SENSOR_NAME_1 = "test_sensor_name_1" +TEST_SENSOR_NAME_2 = "test_sensor_name_2" +TEST_SENSOR_ID_1 = "test_sensor_id_1" +TEST_SENSOR_ID_2 = "test_sensor_id_2" +TEST_SENSOR_UPDATE_INTERVAL = "test_sensor_update_interval" + + +@pytest.fixture(name="basic_wifi") +def fixture_basic_wifi(): + return { + CONF_SSID: TEST_PACKAGE_WIFI_SSID, + CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD, + } + + +@pytest.fixture(name="basic_esphome") +def fixture_basic_esphome(): + return {CONF_NAME: TEST_DEVICE_NAME, CONF_PLATFORM: TEST_PLATFORM} + + +def test_package_unused(basic_esphome, basic_wifi): + """ + Ensures do_package_pass does not change a config if packages aren't used. + """ + config = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + actual = do_packages_pass(config) + assert actual == config + + +def test_package_invalid_dict(basic_esphome, basic_wifi): + """ + Ensures an error is raised if packages is not valid. + + """ + config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi} + + with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +def test_package_include(basic_wifi, basic_esphome): + """ + Tests the simple case where an independent config present in a package is added to the top-level config as is. + + In this test, the CONF_WIFI config is expected to be simply added to the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + } + + expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_append(basic_wifi, basic_esphome): + """ + Tests the case where a key is present in both a package and top-level config. + + In this test, CONF_WIFI is defined in a package, and CONF_DOMAIN is added to it at the top level. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: {CONF_DOMAIN: TEST_DOMAIN}, + } + + expected = { + CONF_ESPHOME: basic_esphome, + CONF_WIFI: { + CONF_SSID: TEST_PACKAGE_WIFI_SSID, + CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD, + CONF_DOMAIN: TEST_DOMAIN, + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_override(basic_wifi, basic_esphome): + """ + Ensures that the top-level configuration takes precedence over duplicate keys defined in a package. + + In this test, CONF_SSID should be overwritten by that defined in the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: {CONF_SSID: TEST_WIFI_SSID}, + } + + expected = { + CONF_ESPHOME: basic_esphome, + CONF_WIFI: { + CONF_SSID: TEST_WIFI_SSID, + CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD, + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_multiple_package_order(): + """ + Ensures that mutiple packages are merged in order. + """ + config = { + CONF_PACKAGES: { + "package1": { + "logger": { + "level": "DEBUG", + }, + }, + "package2": { + "logger": { + "level": "VERBOSE", + }, + }, + }, + } + + expected = { + "logger": { + "level": "VERBOSE", + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_list_merge(): + """ + Ensures lists defined in both a package and the top-level config are merged correctly + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + }, + CONF_SENSOR: [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ], + } + + expected = { + CONF_SENSOR: [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: TEST_SENSOR_NAME_2}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_list_merge_by_id(): + """ + Ensures that components with matching IDs are merged correctly. + + In this test, a sensor is defined in a package, and a CONF_UPDATE_INTERVAL is added at the top level, + and a sensor name is overridden in another sensor. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + "package2": { + CONF_SENSOR: [ + { + CONF_ID: Extend(TEST_SENSOR_ID_1), + CONF_DOMAIN: "2", + } + ], + }, + "package3": { + CONF_SENSOR: [ + { + CONF_ID: Extend(TEST_SENSOR_ID_1), + CONF_DOMAIN: "3", + } + ], + }, + }, + CONF_SENSOR: [ + { + CONF_ID: Extend(TEST_SENSOR_ID_1), + CONF_UPDATE_INTERVAL: TEST_SENSOR_UPDATE_INTERVAL, + }, + {CONF_ID: Extend(TEST_SENSOR_ID_2), CONF_NAME: TEST_SENSOR_NAME_1}, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + CONF_UPDATE_INTERVAL: TEST_SENSOR_UPDATE_INTERVAL, + CONF_DOMAIN: "3", + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2}, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_merge_by_id_with_list(): + """ + Ensures that components with matching IDs are merged correctly when their configuration contains lists. + + For example, a sensor with filters defined in both a package and the top level config should be merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]} + ] + } + }, + CONF_SENSOR: [ + {CONF_ID: Extend(TEST_SENSOR_ID_1), CONF_FILTERS: [{CONF_OFFSET: 146.0}]} + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}, {CONF_OFFSET: 146.0}], + } + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_merge_by_missing_id(): + """ + Ensures that components with missing IDs are not merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]}, + ] + } + }, + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}]}, + {CONF_ID: Extend(TEST_SENSOR_ID_2), CONF_FILTERS: [{CONF_OFFSET: 146.0}]}, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}], + }, + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], + }, + { + CONF_ID: Extend(TEST_SENSOR_ID_2), + CONF_FILTERS: [{CONF_OFFSET: 146.0}], + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 8cc1838d94..236b9f5fc2 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -3,16 +3,16 @@ // matter at all, as long as it compiles). // Not used during runtime nor for CI. -#include -#include -#include -#include #include +#include +#include +#include +#include using namespace esphome; void setup() { - App.pre_setup("livingroom", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/test1.yaml b/tests/test1.yaml index 88cdb43fca..a77f8802b9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -222,6 +222,12 @@ uart: rx_pin: GPIO26 baud_rate: 115200 rx_buffer_size: 1024 + - id: ld2410_uart + tx_pin: 18 + rx_pin: 23 + baud_rate: 256000 + parity: NONE + stop_bits: 1 ota: safe_mode: true @@ -568,6 +574,15 @@ sensor: - platform: duty_cycle pin: GPIO25 name: Duty Cycle Sensor + - platform: ee895 + co2: + name: Office CO2 1 + temperature: + name: Office Temperature 1 + pressure: + name: Office Pressure 1 + address: 0x5F + i2c_id: i2c_bus - platform: esp32_hall name: ESP32 Hall Sensor update_interval: 15s @@ -638,6 +653,13 @@ sensor: temperature: name: Honeywell temperature cs_pin: GPIO5 + - platform: hte501 + temperature: + name: Office Temperature 2 + humidity: + name: Office Humidity 1 + address: 0x40 + i2c_id: i2c_bus - platform: qmc5883l address: 0x0D field_strength_x: @@ -1040,6 +1062,10 @@ sensor: name: tsl2591 calculated_lux id: tsl2591_cl i2c_id: i2c_bus + - platform: tee501 + name: Office Temperature 3 + address: 0x48 + i2c_id: i2c_bus - platform: ultrasonic trigger_pin: GPIO25 echo_pin: @@ -1050,6 +1076,8 @@ sensor: id: ultrasonic_sensor1 - platform: uptime name: Uptime Sensor + - id: !extend ${devicename}_uptime_pcg + unit_of_measurement: s - platform: wifi_signal name: WiFi Signal Sensor update_interval: 15s @@ -1180,6 +1208,17 @@ sensor: pressure: name: "MPL3115A2 Pressure" update_interval: 10s + - platform: ld2410 + moving_distance: + name: "Moving distance (cm)" + still_distance: + name: "Still Distance (cm)" + moving_energy: + name: "Move Energy" + still_energy: + name: "Still Energy" + detection_distance: + name: "Distance Detection" esp32_touch: setup_mode: false @@ -1284,6 +1323,11 @@ binary_sensor: number: GPIO9 mode: INPUT_PULLUP name: Living Room Window 2 + - platform: gpio + pin: + number: GPIO9 + mode: INPUT_OUTPUT_OPEN_DRAIN + name: Living Room Button - platform: status name: Living Room Status - platform: esp32_touch @@ -1341,6 +1385,13 @@ binary_sensor: number: 1 mode: INPUT inverted: true + - platform: gpio + name: PCA9554 binary sensor + pin: + pca9554: pca9554_hub + number: 1 + mode: INPUT + inverted: true - platform: gpio name: MCP21 binary sensor pin: @@ -1434,6 +1485,13 @@ binary_sensor: id: close_sensor - platform: template id: close_obstacle_sensor + - platform: ld2410 + has_target: + name: presence + has_moving_target: + name: movement + has_still_target: + name: still pca9685: frequency: 500 @@ -1458,6 +1516,28 @@ my9231: num_chips: 2 bit_depth: 16 +sm2235: + data_pin: GPIO4 + clock_pin: GPIO5 + max_power_color_channels: 9 + max_power_white_channels: 9 + +sm2335: + data_pin: GPIO4 + clock_pin: GPIO5 + max_power_color_channels: 9 + max_power_white_channels: 9 + +bp1658cj: + data_pin: GPIO3 + clock_pin: GPIO5 + max_power_color_channels: 4 + max_power_white_channels: 6 + +bp5758d: + data_pin: GPIO3 + clock_pin: GPIO5 + output: - platform: gpio pin: GPIO26 @@ -1537,6 +1617,13 @@ output: number: 0 mode: OUTPUT inverted: false + - platform: gpio + id: id26 + pin: + pca9554: pca9554_hub + number: 0 + mode: OUTPUT + inverted: false - platform: gpio id: id22 pin: @@ -1576,6 +1663,36 @@ output: - platform: my9231 id: my_5 channel: 5 + - platform: sm2235 + id: sm2235_red + channel: 1 + - platform: sm2235 + id: sm2235_green + channel: 0 + - platform: sm2235 + id: sm2235_blue + channel: 2 + - platform: sm2235 + id: sm2235_coldwhite + channel: 4 + - platform: sm2235 + id: sm2235_warmwhite + channel: 3 + - platform: sm2335 + id: sm2335_red + channel: 1 + - platform: sm2335 + id: sm2335_green + channel: 0 + - platform: sm2335 + id: sm2335_blue + channel: 2 + - platform: sm2335 + id: sm2335_coldwhite + channel: 4 + - platform: sm2335 + id: sm2335_warmwhite + channel: 3 - platform: slow_pwm id: id24 pin: GPIO26 @@ -1612,6 +1729,47 @@ output: vref: internal gain: X2 power_down: gnd_500k + - platform: bp1658cj + id: bp1658cj_red + channel: 1 + - platform: bp1658cj + id: bp1658cj_green + channel: 2 + - platform: bp1658cj + id: bp1658cj_blue + channel: 0 + - platform: bp1658cj + id: bp1658cj_coldwhite + channel: 3 + - platform: bp1658cj + id: bp1658cj_warmwhite + channel: 4 + - platform: bp5758d + id: bp5758d_red + channel: 2 + current: 10 + - platform: bp5758d + id: bp5758d_green + channel: 3 + current: 10 + - platform: bp5758d + id: bp5758d_blue + channel: 1 + current: 10 + - platform: bp5758d + id: bp5758d_coldwhite + channel: 5 + current: 10 + - platform: bp5758d + id: bp5758d_warmwhite + channel: 4 + current: 10 + - platform: x9c + id: test_x9c + cs_pin: GPIO25 + inc_pin: GPIO26 + ud_pin: GPIO27 + initial_value: 0.5 e131: @@ -2716,6 +2874,11 @@ pcf8574: pcf8575: false i2c_id: i2c_bus +pca9554: + - id: pca9554_hub + address: 0x3F + i2c_id: i2c_bus + mcp23017: - id: mcp23017_hub open_drain_interrupt: true @@ -3011,6 +3174,19 @@ button: on_press: midea_ac.power_toggle: +ld2410: + id: my_ld2410 + uart_id: ld2410_uart + timeout: 150s + max_move_distance: 6m + max_still_distance: 0.75m + g0_move_threshold: 10 + g0_still_threshold: 20 + g2_move_threshold: 20 + g2_still_threshold: 21 + g8_move_threshold: 80 + g8_still_threshold: 81 + lcd_menu: display_id: my_lcd_gpio mark_back: 0x5e diff --git a/tests/test2.yaml b/tests/test2.yaml index 7d4cb4cbb2..3e6a186320 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -381,6 +381,20 @@ sensor: temperature_sensor: ha_hello_world_temperature ph: name: Ufire pH + - platform: mics_4514 + update_interval: 60s + nitrogen_dioxide: + name: MICS-4514 NO2 + carbon_monoxide: + name: MICS-4514 CO + methane: + name: MICS-4514 CH4 + hydrogen: + name: MICS-4514 H2 + ethanol: + name: MICS-4514 C2H5OH + ammonia: + name: MICS-4514 NH3 time: - platform: homeassistant @@ -462,6 +476,15 @@ binary_sensor: name: Mi Motion Sensor 2 Light button: name: Mi Motion Sensor 2 Button + - platform: gpio + id: gpio_set_retry_test + pin: GPIO9 + on_press: + then: + - lambda: |- + App.scheduler.set_retry(id(gpio_set_retry_test), "set_retry_test", 100, 3, [](const uint8_t remaining) { + return remaining ? RetryResult::RETRY : RetryResult::DONE; // just to reference both symbols + }, 5.0f); esp32_ble_tracker: on_ble_advertise: diff --git a/tests/test3.yaml b/tests/test3.yaml index 9b4e530fbd..4827b7cbcd 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -287,6 +287,9 @@ uart: modbus: uart_id: uart1 +vbus: + uart_id: uart4 + ota: safe_mode: true port: 3286 @@ -298,6 +301,7 @@ logger: esp8266_store_log_strings_in_flash: true improv_serial: + next_url: https://esphome.io/?name={{device_name}}&version={{esphome_version}}&ip={{ip_address}} deep_sleep: run_duration: 20s @@ -798,6 +802,11 @@ sensor: id: adc128s102_channel_0 channel: 0 + - platform: vbus + model: deltasol c + temperature_1: + name: Temperature 1 + time: - platform: homeassistant @@ -903,6 +912,11 @@ binary_sensor: then: - pzemac.reset_energy: pzemac1 + - platform: vbus + model: deltasol_bs_plus + relay1: + name: Relay 1 On + globals: - id: my_global_string type: std::string @@ -1122,14 +1136,16 @@ climate: - switch.turn_on: gpio_switch1 fan_mode_diffuse_action: - switch.turn_on: gpio_switch2 + fan_mode_quiet_action: + - switch.turn_on: gpio_switch1 swing_off_action: - - switch.turn_on: gpio_switch1 + - switch.turn_on: gpio_switch2 swing_horizontal_action: - - switch.turn_on: gpio_switch2 - swing_vertical_action: - switch.turn_on: gpio_switch1 - swing_both_action: + swing_vertical_action: - switch.turn_on: gpio_switch2 + swing_both_action: + - switch.turn_on: gpio_switch1 startup_delay: true supplemental_cooling_delta: 2.0 cool_deadband: 0.5 @@ -1313,6 +1329,21 @@ output: return {s}; outputs: - id: custom_binary + - platform: sigma_delta_output + id: sddac + update_interval: 60s + pin: D4 + turn_on_action: + then: + - logger.log: "Turned on" + turn_off_action: + then: + - logger.log: "Turned off" + state_change_action: + then: + - logger.log: + format: "Changed state: %d" + args: [ 'state' ] - platform: custom type: float lambda: |- diff --git a/tests/test5.yaml b/tests/test5.yaml index 596ba5de4e..5f72579d08 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -66,6 +66,9 @@ mqtt: ESP_LOGD("Mqtt Test", "testing/sensor/testing_sensor/state=[%s]", x.c_str()); # yamllint enable rule:line-length +vbus: + - uart_id: uart2 + binary_sensor: - platform: gpio pin: GPIO0 @@ -168,14 +171,38 @@ binary_sensor: sn74hc165: sn74hc165_hub number: 0 - - - platform: ezo_pmp pump_state: name: "Pump State" is_paused: name: "Is Paused" + - platform: matrix_keypad + keypad_id: keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + + - platform: vbus + model: deltasol_bs_plus + relay2: + name: Relay 2 On + sensor1_error: + name: Sensor 1 Error + + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + binary_sensors: + - id: vcustom_b + name: VBus Custom Binary Sensor + lambda: return x[0] & 1; + tlc5947: data_pin: GPIO12 clock_pin: GPIO14 @@ -470,6 +497,27 @@ sensor: max_flow_rate: name: Max Flow Rate + - platform: vbus + model: deltasol c + temperature_3: + name: Temperature 3 + operating_hours_1: + name: Operating Hours 1 + heat_quantity: + name: Heat Quantity + time: + name: System Time + + - platform: vbus + model: custom + command: 0x100 + source: 0x1234 + dest: 0x10 + sensors: + - id: vcustom + name: VBus Custom Sensor + lambda: return x[0] / 10.0; + script: - id: automation_test then: @@ -517,6 +565,9 @@ display: lambda: |- it.print("81818181"); +time: + - platform: pcf85063 + text_sensor: - platform: ezo_pmp dosing_mode: @@ -555,3 +606,21 @@ sn74hc165: load_pin: GPIO27 clock_inhibit_pin: GPIO26 sr_count: 4 + + +matrix_keypad: + id: keypad + rows: + - pin: 21 + - pin: 19 + columns: + - pin: 17 + - pin: 16 + keys: "1234" + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 + diff --git a/tests/test7.yaml b/tests/test7.yaml new file mode 100644 index 0000000000..10e1b035ab --- /dev/null +++ b/tests/test7.yaml @@ -0,0 +1,33 @@ +# Tests for ESP32-C3 boards which use toolchain-riscv32-esp +--- +wifi: + ssid: 'ssid' + +esp32: + board: lolin_c3_mini + framework: + type: arduino + +esphome: + name: 'on-response-test' + on_boot: + then: + - http_request.send: + method: PUT + url: https://esphome.io + headers: + Content-Type: application/json + body: Some data + verify_ssl: false + on_response: + then: + - logger.log: + format: "Response status: %d" + args: + - status_code + +logger: + +http_request: + useragent: esphome/tagreader + timeout: 10s