diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d73adbfa30..c67378093e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,3 +7,8 @@ updates: ignore: # Hypotehsis is only used for testing and is updated quite often - dependency-name: hypothesis + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 1d1cc169b2..d424bd3b60 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -30,15 +30,15 @@ jobs: arch: [amd64, armv7, aarch64] build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set TAG run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2ad769156..1705610947 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,15 +75,15 @@ jobs: pio_cache_key: tidyesp32-idf steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 id: python with: python-version: '3.8' - name: Cache virtualenv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: .venv key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} @@ -102,7 +102,7 @@ jobs: # Use per check platformio cache because checks use different parts - name: Cache platformio - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -131,7 +131,7 @@ jobs: if: matrix.id == 'ci-custom' - name: Lint Python - run: script/lint-python + run: script/lint-python -a if: matrix.id == 'lint-python' - run: esphome compile ${{ matrix.file }} @@ -163,4 +163,4 @@ jobs: - name: Suggested changes run: script/ci-suggest-changes - if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format') + if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5281c2fb7..216c094122 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: outputs: tag: ${{ steps.tag.outputs.tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Get tag id: tag run: | @@ -35,9 +35,9 @@ jobs: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: '3.x' - name: Set up python environment @@ -65,24 +65,24 @@ jobs: arch: [amd64, armv7, aarch64] build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Log in to docker hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} @@ -108,9 +108,9 @@ jobs: matrix: build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Enable experimental manifest support @@ -119,12 +119,12 @@ jobs: echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - name: Log in to docker hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c3e450d0cf..b998043039 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v4 + - uses: actions/stale@v5 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -35,7 +35,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v4 + - uses: actions/stale@v5 with: days-before-pr-stale: -1 days-before-pr-close: -1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e666f20af..95365ff5bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/ambv/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: @@ -26,7 +26,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/CODEOWNERS b/CODEOWNERS index 3cb38fce06..62aae6d3cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,6 +52,7 @@ esphome/components/cs5460a/* @balrog-kun esphome/components/cse7761/* @berfenger esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx +esphome/components/dac7678/* @NickB1 esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter @@ -72,6 +73,7 @@ esphome/components/esp8266/* @esphome/core esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter +esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core @@ -188,9 +190,11 @@ esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/sml/* @alengwenus +esphome/components/smt100/* @piechade esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core +esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 @@ -236,6 +240,7 @@ 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/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/docker/Dockerfile b/docker/Dockerfile index dc8ba03f48..f4652bc513 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -46,12 +46,10 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.37.1 \ - platformio==5.2.5 \ + platformio==6.0.2 \ # Change some platformio settings && platformio settings set enable_telemetry No \ - && platformio settings set check_libraries_interval 1000000 \ && platformio settings set check_platformio_interval 1000000 \ - && platformio settings set check_platforms_interval 1000000 \ && mkdir -p /piolibs @@ -96,7 +94,7 @@ RUN \ apt-get update \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ - nginx-light=1.18.0-6.1 \ + nginx-light=1.18.0-6.1+deb11u2 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ @@ -136,7 +134,7 @@ RUN \ clang-tidy-11=1:11.0.1-2 \ patch=2.7.6-7 \ software-properties-common=0.96.20.2-2.1 \ - nano=5.4-2 \ + nano=5.4-2+deb11u1 \ build-essential=12.9 \ python3-dev=3.9.2-3 \ && rm -rf \ diff --git a/esphome/automation.py b/esphome/automation.py index 4007dc4c51..4aede00c5e 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -12,7 +12,7 @@ from esphome.const import ( CONF_TYPE_ID, CONF_TIME, ) -from esphome.jsonschema import jschema_extractor +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry @@ -23,11 +23,10 @@ def maybe_simple_id(*validators): def maybe_conf(conf, *validators): validator = cv.All(*validators) - @jschema_extractor("maybe") + @schema_extractor("maybe") def validate(value): - # pylint: disable=comparison-with-callable - if value == jschema_extractor: - return validator + if value == SCHEMA_EXTRACT: + return (validator, conf) if isinstance(value, dict): return validator(value) @@ -111,11 +110,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): # This should only happen with invalid configs, but let's have a nice error message. return [schema(value)] - @jschema_extractor("automation") + @schema_extractor("automation") def validator(value): - # hack to get the schema - # pylint: disable=comparison-with-callable - if value == jschema_extractor: + if value == SCHEMA_EXTRACT: return schema value = validator_(value) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index daa1d3c5cf..5a29d86404 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -62,10 +62,6 @@ void ADCSensor::setup() { } } - // adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2 -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2) - adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); -#endif #endif // USE_ESP32 } diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 39b1187caf..6c8316d338 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -92,7 +92,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } if (this->codec_->has_unit()) { this->fahrenheit_ = (this->codec_->unit_ == 'f'); - ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius"); + ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius"); this->current_request_++; } this->publish_state(); diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 3e9a62f3d8..88a74540d0 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -473,6 +473,7 @@ enum SensorStateClass { STATE_CLASS_NONE = 0; STATE_CLASS_MEASUREMENT = 1; STATE_CLASS_TOTAL_INCREASING = 2; + STATE_CLASS_TOTAL = 3; } enum SensorLastResetType { diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 9f0ab82a52..b19a55764f 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -270,7 +270,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { * * If the handshake is still active when this method returns and a read/write can't take place at * the moment, returns WOULD_BLOCK. - * If an error occured, returns that error. Only returns OK if the transport is ready for data + * If an error occurred, returns that error. Only returns OK if the transport is ready for data * traffic. */ APIError APINoiseFrameHelper::state_action_() { @@ -586,7 +586,7 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { } return APIError::OK; } else if (sent == -1) { - // an error occured + // an error occurred state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", errno); return APIError::SOCKET_WRITE_FAILED; @@ -980,7 +980,7 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt } return APIError::OK; } else if (sent == -1) { - // an error occured + // an error occurred state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", errno); return APIError::SOCKET_WRITE_FAILED; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 70f909c07a..b91c9bd600 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -108,6 +108,8 @@ template<> const char *proto_enum_to_string(enums::Sens return "STATE_CLASS_MEASUREMENT"; case enums::STATE_CLASS_TOTAL_INCREASING: return "STATE_CLASS_TOTAL_INCREASING"; + case enums::STATE_CLASS_TOTAL: + return "STATE_CLASS_TOTAL"; default: return "UNKNOWN"; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ec1cdc35ac..f9981fdbb7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -53,6 +53,7 @@ enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, + STATE_CLASS_TOTAL = 3, }; enum SensorLastResetType : uint32_t { LAST_RESET_NONE = 0, diff --git a/esphome/components/bedjet/__init__.py b/esphome/components/bedjet/__init__.py index 16821fc016..1697c549b3 100644 --- a/esphome/components/bedjet/__init__.py +++ b/esphome/components/bedjet/__init__.py @@ -1 +1,52 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ble_client, time +from esphome.const import ( + CONF_ID, + CONF_RECEIVE_TIMEOUT, + CONF_TIME_ID, +) + CODEOWNERS = ["@jhansche"] +DEPENDENCIES = ["ble_client"] +MULTI_CONF = True +CONF_BEDJET_ID = "bedjet_id" + +bedjet_ns = cg.esphome_ns.namespace("bedjet") +BedJetHub = bedjet_ns.class_("BedJetHub", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BedJetHub), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional( + CONF_RECEIVE_TIMEOUT, default="0s" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("15s")) +) + +BEDJET_CLIENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_BEDJET_ID): cv.use_id(BedJetHub), + } +) + + +async def register_bedjet_child(var, config): + parent = await cg.get_variable(config[CONF_BEDJET_ID]) + cg.add(parent.register_child(var)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + if CONF_TIME_ID in config: + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time_id(time_)) + if CONF_RECEIVE_TIMEOUT in config: + cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp deleted file mode 100644 index 60eef52334..0000000000 --- a/esphome/components/bedjet/bedjet.cpp +++ /dev/null @@ -1,675 +0,0 @@ -#include "bedjet.h" -#include "esphome/core/log.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace bedjet { - -using namespace esphome::climate; - -/// Converts a BedJet temp step into degrees Celsius. -float bedjet_temp_to_c(const uint8_t temp) { - // BedJet temp is "C*2"; to get C, divide by 2. - return temp / 2.0f; -} - -/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. -uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { - // 0 = 5% - // 19 = 100% - return 5 * fan + 5; -} - -static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { - if (fan_step >= 0 && fan_step <= 19) - return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; - return nullptr; -} - -static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { - for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { - if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { - return i; - } - } - return -1; -} - -static BedjetButton heat_button(BedjetHeatMode mode) { - BedjetButton btn = BTN_HEAT; - if (mode == HEAT_MODE_EXTENDED) { - btn = BTN_EXTHT; - } - return btn; -} - -void Bedjet::upgrade_firmware() { - auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); - auto status = this->write_bedjet_packet_(pkt); - - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } -} - -void Bedjet::dump_config() { - LOG_CLIMATE("", "BedJet Climate", this); - auto traits = this->get_traits(); - - ESP_LOGCONFIG(TAG, " Supported modes:"); - for (auto mode : traits.get_supported_modes()) { - ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode))); - } - - ESP_LOGCONFIG(TAG, " Supported fan modes:"); - for (const auto &mode : traits.get_supported_fan_modes()) { - ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); - } - for (const auto &mode : traits.get_supported_custom_fan_modes()) { - ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); - } - - ESP_LOGCONFIG(TAG, " Supported presets:"); - for (auto preset : traits.get_supported_presets()) { - ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); - } - for (const auto &preset : traits.get_supported_custom_presets()) { - ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); - } -} - -void Bedjet::setup() { - this->codec_ = make_unique(); - - // restore set points - auto restore = this->restore_state_(); - if (restore.has_value()) { - ESP_LOGI(TAG, "Restored previous saved state."); - restore->apply(this); - } else { - // Initial status is unknown until we connect - this->reset_state_(); - } - -#ifdef USE_TIME - this->setup_time_(); -#endif -} - -/** Resets states to defaults. */ -void Bedjet::reset_state_() { - this->mode = climate::CLIMATE_MODE_OFF; - this->action = climate::CLIMATE_ACTION_IDLE; - this->target_temperature = NAN; - this->current_temperature = NAN; - this->preset.reset(); - this->custom_preset.reset(); - this->publish_state(); -} - -void Bedjet::loop() {} - -void Bedjet::control(const ClimateCall &call) { - ESP_LOGD(TAG, "Received Bedjet::control"); - if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); - return; - } - - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); - BedjetPacket *pkt; - switch (mode) { - case climate::CLIMATE_MODE_OFF: - pkt = this->codec_->get_button_request(BTN_OFF); - break; - case climate::CLIMATE_MODE_HEAT: - pkt = this->codec_->get_button_request(heat_button(this->heating_mode_)); - break; - case climate::CLIMATE_MODE_FAN_ONLY: - pkt = this->codec_->get_button_request(BTN_COOL); - break; - case climate::CLIMATE_MODE_DRY: - pkt = this->codec_->get_button_request(BTN_DRY); - break; - default: - ESP_LOGW(TAG, "Unsupported mode: %d", mode); - return; - } - - auto status = this->write_bedjet_packet_(pkt); - - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } else { - this->force_refresh_ = true; - this->mode = mode; - // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those - this->custom_preset.reset(); - this->preset.reset(); - } - } - - if (call.get_target_temperature().has_value()) { - auto target_temp = *call.get_target_temperature(); - auto *pkt = this->codec_->get_set_target_temp_request(target_temp); - auto status = this->write_bedjet_packet_(pkt); - - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } else { - this->target_temperature = target_temp; - } - } - - if (call.get_preset().has_value()) { - ClimatePreset preset = *call.get_preset(); - BedjetPacket *pkt; - - if (preset == climate::CLIMATE_PRESET_BOOST) { - pkt = this->codec_->get_button_request(BTN_TURBO); - } else { - ESP_LOGW(TAG, "Unsupported preset: %d", preset); - return; - } - - auto status = this->write_bedjet_packet_(pkt); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } else { - // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. - this->mode = climate::CLIMATE_MODE_HEAT; - this->preset = preset; - this->custom_preset.reset(); - this->force_refresh_ = true; - } - } else if (call.get_custom_preset().has_value()) { - std::string preset = *call.get_custom_preset(); - BedjetPacket *pkt; - - if (preset == "M1") { - pkt = this->codec_->get_button_request(BTN_M1); - } else if (preset == "M2") { - pkt = this->codec_->get_button_request(BTN_M2); - } else if (preset == "M3") { - pkt = this->codec_->get_button_request(BTN_M3); - } else if (preset == "LTD HT") { - pkt = this->codec_->get_button_request(BTN_HEAT); - } else if (preset == "EXT HT") { - pkt = this->codec_->get_button_request(BTN_EXTHT); - } else { - ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); - return; - } - - auto status = this->write_bedjet_packet_(pkt); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } else { - this->force_refresh_ = true; - this->custom_preset = preset; - this->preset.reset(); - } - } - - if (call.get_fan_mode().has_value()) { - // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. - // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. - auto fan_mode = *call.get_fan_mode(); - BedjetPacket *pkt; - if (fan_mode == climate::CLIMATE_FAN_LOW) { - pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */); - } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) { - pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */); - } else if (fan_mode == climate::CLIMATE_FAN_HIGH) { - pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */); - } else { - ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), - LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); - return; - } - - auto status = this->write_bedjet_packet_(pkt); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } else { - this->force_refresh_ = true; - } - } else if (call.get_custom_fan_mode().has_value()) { - auto fan_mode = *call.get_custom_fan_mode(); - auto fan_step = bedjet_fan_speed_to_step(fan_mode); - if (fan_step >= 0 && fan_step <= 19) { - ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), - fan_step); - // The index should represent the fan_step index. - BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step); - auto status = this->write_bedjet_packet_(pkt); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); - } else { - this->force_refresh_ = true; - } - } - } -} - -void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - switch (event) { - case ESP_GATTC_DISCONNECT_EVT: { - ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); - this->status_set_warning(); - break; - } - case ESP_GATTC_SEARCH_CMPL_EVT: { - auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); - if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); - break; - } - this->char_handle_cmd_ = chr->handle; - - chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); - if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); - break; - } - - this->char_handle_status_ = chr->handle; - // We also need to obtain the config descriptor for this handle. - // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be - // able to look it up. - auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); - if (descr == nullptr) { - ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", - this->char_handle_status_); - } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || - descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { - ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, - descr->uuid.to_string().c_str()); - } else { - this->config_descr_status_ = descr->handle; - } - - chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); - if (chr != nullptr) { - this->char_handle_name_ = chr->handle; - auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, - ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); - } - } - - ESP_LOGD(TAG, "Services complete: obtained char handles."); - this->node_state = espbt::ClientState::ESTABLISHED; - - this->set_notify_(true); - -#ifdef USE_TIME - if (this->time_id_.has_value()) { - this->send_local_time(); - } -#endif - break; - } - case ESP_GATTC_WRITE_DESCR_EVT: { - if (param->write.status != ESP_GATT_OK) { - // ESP_GATT_INVALID_ATTR_LEN - ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status); - break; - } - // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0 - // This might be the enable-notify descriptor? (or disable-notify) - ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, - param->write.status); - break; - } - case ESP_GATTC_WRITE_CHAR_EVT: { - if (param->write.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); - break; - } - if (param->write.handle == this->char_handle_cmd_) { - if (this->force_refresh_) { - // Command write was successful. Publish the pending state, hoping that notify will kick in. - this->publish_state(); - } - } - break; - } - case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent_->conn_id) - break; - if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); - break; - } - if (param->read.handle == this->char_handle_status_) { - // This is the additional packet that doesn't fit in the notify packet. - this->codec_->decode_extra(param->read.value, param->read.value_len); - } else if (param->read.handle == this->char_handle_name_) { - // The data should represent the name. - if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { - std::string bedjet_name(reinterpret_cast(param->read.value), param->read.value_len); - // this->set_name(bedjet_name); - ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); - } - } - break; - } - case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - // This event means that ESP received the request to enable notifications on the client side. But we also have to - // tell the server that we want it to send notifications. Normally BLEClient parent would handle this - // automatically, but as soon as we set our status to Established, the parent is going to purge all the - // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable - // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write - // doesn't break anything. - - if (param->reg_for_notify.handle != this->char_handle_status_) { - ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", - this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); - break; - } - - this->write_notify_config_descriptor_(true); - this->last_notify_ = 0; - this->force_refresh_ = true; - break; - } - case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - // This event is not handled by the parent BLEClient, so we need to do this either way. - if (param->unreg_for_notify.handle != this->char_handle_status_) { - ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", - this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); - break; - } - - this->write_notify_config_descriptor_(false); - this->last_notify_ = 0; - // Now we wait until the next update() poll to re-register notify... - break; - } - case ESP_GATTC_NOTIFY_EVT: { - if (param->notify.handle != this->char_handle_status_) { - ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), - this->char_handle_status_, param->notify.handle); - break; - } - - // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we - // throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). - // Another idea would be to keep notify off by default, and use update() as an opportunity to turn on - // notify to get enough data to update status, then turn off notify again. - - uint32_t now = millis(); - auto delta = now - this->last_notify_; - - if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { - bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); - this->last_notify_ = now; - - if (needs_extra) { - // this means the packet was partial, so read the status characteristic to get the second part. - auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, - this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); - } - } - - if (this->force_refresh_) { - // If we requested an immediate update, do that now. - this->update(); - this->force_refresh_ = false; - } - } - break; - } - default: - ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); - break; - } -} - -/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. - * - * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order - * to undo the same on unregister. It also allows us to maintain the config descriptor separately, - * since the parent BLEClient is going to purge all descriptors once we set our connection status - * to `Established`. - */ -uint8_t Bedjet::write_notify_config_descriptor_(bool enable) { - auto handle = this->config_descr_status_; - if (handle == 0) { - ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); - return -1; - } - - // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. - uint8_t notify_en[] = {0, 0}; - notify_en[0] = enable; - auto status = - esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), - ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); - return status; - } - ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false", - handle); - return ESP_GATT_OK; -} - -#ifdef USE_TIME -/** Attempts to sync the local time (via `time_id`) to the BedJet device. */ -void Bedjet::send_local_time() { - if (this->time_id_.has_value()) { - auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); - if (now.is_valid()) { - this->set_clock(now.hour, now.minute); - ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); - } - } else { - ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); - } -} - -/** Initializes time sync callbacks to support syncing current time to the BedJet. */ -void Bedjet::setup_time_() { - if (this->time_id_.has_value()) { - this->send_local_time(); - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); - } else { - ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); - } -} -#endif - -/** Attempt to set the BedJet device's clock to the specified time. */ -void Bedjet::set_clock(uint8_t hour, uint8_t minute) { - if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); - return; - } - - BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); - auto status = this->write_bedjet_packet_(pkt); - if (status) { - ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); - } else { - ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); - } -} - -/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */ -uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) { - if (this->node_state != espbt::ClientState::ESTABLISHED) { - if (!this->parent_->enabled) { - ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); - } else { - ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); - } - return -1; - } - auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, - pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); - return status; -} - -/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ -uint8_t Bedjet::set_notify_(const bool enable) { - uint8_t status; - if (enable) { - status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, - this->char_handle_status_); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); - } - } else { - status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, - this->char_handle_status_); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); - } - } - ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); - return status; -} - -/** Attempts to update the climate device from the last received BedjetStatusPacket. - * - * @return `true` if the status has been applied; `false` if there is nothing to apply. - */ -bool Bedjet::update_status_() { - if (!this->codec_->has_status()) - return false; - - BedjetStatusPacket status = *this->codec_->get_status_packet(); - - auto converted_temp = bedjet_temp_to_c(status.target_temp_step); - if (converted_temp > 0) - this->target_temperature = converted_temp; - converted_temp = bedjet_temp_to_c(status.ambient_temp_step); - if (converted_temp > 0) - this->current_temperature = converted_temp; - - const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step); - if (fan_mode_name != nullptr) { - this->custom_fan_mode = *fan_mode_name; - } - - // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. - switch (status.mode) { - case MODE_WAIT: // Biorhythm "wait" step: device is idle - case MODE_STANDBY: - this->mode = climate::CLIMATE_MODE_OFF; - this->action = climate::CLIMATE_ACTION_IDLE; - this->fan_mode = climate::CLIMATE_FAN_OFF; - this->custom_preset.reset(); - this->preset.reset(); - break; - - case MODE_HEAT: - this->mode = climate::CLIMATE_MODE_HEAT; - this->action = climate::CLIMATE_ACTION_HEATING; - this->preset.reset(); - if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - this->set_custom_preset_("LTD HT"); - } else { - this->custom_preset.reset(); - } - break; - - case MODE_EXTHT: - this->mode = climate::CLIMATE_MODE_HEAT; - this->action = climate::CLIMATE_ACTION_HEATING; - this->preset.reset(); - if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - this->custom_preset.reset(); - } else { - this->set_custom_preset_("EXT HT"); - } - break; - - case MODE_COOL: - this->mode = climate::CLIMATE_MODE_FAN_ONLY; - this->action = climate::CLIMATE_ACTION_COOLING; - this->custom_preset.reset(); - this->preset.reset(); - break; - - case MODE_DRY: - this->mode = climate::CLIMATE_MODE_DRY; - this->action = climate::CLIMATE_ACTION_DRYING; - this->custom_preset.reset(); - this->preset.reset(); - break; - - case MODE_TURBO: - this->preset = climate::CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); - this->mode = climate::CLIMATE_MODE_HEAT; - this->action = climate::CLIMATE_ACTION_HEATING; - break; - - default: - ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode); - break; - } - - if (this->is_valid_()) { - this->publish_state(); - this->codec_->clear_status(); - this->status_clear_warning(); - } - - return true; -} - -void Bedjet::update() { - ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str()); - - if (this->node_state != espbt::ClientState::ESTABLISHED) { - if (!this->parent()->enabled) { - ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str()); - } else { - // Possibly still trying to connect. - ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str()); - } - - return; - } - - auto result = this->update_status_(); - if (!result) { - uint32_t now = millis(); - uint32_t diff = now - this->last_notify_; - - if (this->last_notify_ == 0) { - // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. - // However, it could also mean that it's running, but failing to send notifications. - // We can try to unregister for notifications now, and then re-register, hoping to clear it up... - // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? - - ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); - this->set_notify_(false); - } else if (diff > NOTIFY_WARN_THRESHOLD) { - ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); - } - - if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { - ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); - this->parent()->set_enabled(false); - this->parent()->set_enabled(true); - } - } -} - -} // namespace bedjet -} // namespace esphome - -#endif diff --git a/esphome/components/bedjet/bedjet_child.h b/esphome/components/bedjet/bedjet_child.h new file mode 100644 index 0000000000..4e07745c63 --- /dev/null +++ b/esphome/components/bedjet/bedjet_child.h @@ -0,0 +1,23 @@ +#pragma once + +#include "bedjet_codec.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace bedjet { + +// Forward declare BedJetHub +class BedJetHub; + +class BedJetClient : public Parented { + public: + virtual void on_status(const BedjetStatusPacket *data) = 0; + virtual void on_bedjet_state(bool is_ready) = 0; + + protected: + friend BedJetHub; + virtual std::string describe() = 0; +}; + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/bedjet_climate.cpp b/esphome/components/bedjet/bedjet_climate.cpp new file mode 100644 index 0000000000..8d9fdd7318 --- /dev/null +++ b/esphome/components/bedjet/bedjet_climate.cpp @@ -0,0 +1,354 @@ +#include "bedjet_climate.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace bedjet { + +using namespace esphome::climate; + +/// Converts a BedJet temp step into degrees Celsius. +float bedjet_temp_to_c(const uint8_t temp) { + // BedJet temp is "C*2"; to get C, divide by 2. + return temp / 2.0f; +} + +static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { + if (fan_step <= 19) + return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; + return nullptr; +} + +static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { + for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { + if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { + return i; + } + } + return -1; +} + +static inline BedjetButton heat_button(BedjetHeatMode mode) { + return mode == HEAT_MODE_EXTENDED ? BTN_EXTHT : BTN_HEAT; +} + +std::string BedJetClimate::describe() { return "BedJet Climate"; } + +void BedJetClimate::dump_config() { + LOG_CLIMATE("", "BedJet Climate", this); + auto traits = this->get_traits(); + + ESP_LOGCONFIG(TAG, " Supported modes:"); + for (auto mode : traits.get_supported_modes()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode))); + } + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + ESP_LOGCONFIG(TAG, " - BedJet heating mode: EXT HT"); + } else { + ESP_LOGCONFIG(TAG, " - BedJet heating mode: HEAT"); + } + + ESP_LOGCONFIG(TAG, " Supported fan modes:"); + for (const auto &mode : traits.get_supported_fan_modes()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); + } + for (const auto &mode : traits.get_supported_custom_fan_modes()) { + ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); + } + + ESP_LOGCONFIG(TAG, " Supported presets:"); + for (auto preset : traits.get_supported_presets()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); + } + for (const auto &preset : traits.get_supported_custom_presets()) { + ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); + } +} + +void BedJetClimate::setup() { + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + ESP_LOGI(TAG, "Restored previous saved state."); + restore->apply(this); + } else { + // Initial status is unknown until we connect + this->reset_state_(); + } +} + +/** Resets states to defaults. */ +void BedJetClimate::reset_state_() { + this->mode = CLIMATE_MODE_OFF; + this->action = CLIMATE_ACTION_IDLE; + this->target_temperature = NAN; + this->current_temperature = NAN; + this->preset.reset(); + this->custom_preset.reset(); + this->publish_state(); +} + +void BedJetClimate::loop() {} + +void BedJetClimate::control(const ClimateCall &call) { + ESP_LOGD(TAG, "Received BedJetClimate::control"); + if (!this->parent_->is_connected()) { + ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); + return; + } + + if (call.get_mode().has_value()) { + ClimateMode mode = *call.get_mode(); + bool button_result; + switch (mode) { + case CLIMATE_MODE_OFF: + button_result = this->parent_->button_off(); + break; + case CLIMATE_MODE_HEAT: + button_result = this->parent_->send_button(heat_button(this->heating_mode_)); + break; + case CLIMATE_MODE_FAN_ONLY: + button_result = this->parent_->button_cool(); + break; + case CLIMATE_MODE_DRY: + button_result = this->parent_->button_dry(); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %d", mode); + return; + } + + if (button_result) { + this->mode = mode; + // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those + this->custom_preset.reset(); + this->preset.reset(); + } + } + + if (call.get_target_temperature().has_value()) { + auto target_temp = *call.get_target_temperature(); + auto result = this->parent_->set_target_temp(target_temp); + + if (result) { + this->target_temperature = target_temp; + } + } + + if (call.get_preset().has_value()) { + ClimatePreset preset = *call.get_preset(); + bool result; + + if (preset == CLIMATE_PRESET_BOOST) { + // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. + result = this->parent_->button_turbo(); + + if (result) { + this->mode = CLIMATE_MODE_HEAT; + this->preset = CLIMATE_PRESET_BOOST; + this->custom_preset.reset(); + } + } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { + if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { + // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat. + result = this->parent_->send_button(heat_button(this->heating_mode_)); + if (result) { + this->preset.reset(); + this->custom_preset.reset(); + } + } else { + ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", + LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)), + LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE)))); + } + } else { + ESP_LOGW(TAG, "Unsupported preset: %d", preset); + return; + } + } else if (call.get_custom_preset().has_value()) { + std::string preset = *call.get_custom_preset(); + bool result; + + if (preset == "M1") { + result = this->parent_->button_memory1(); + } else if (preset == "M2") { + result = this->parent_->button_memory2(); + } else if (preset == "M3") { + result = this->parent_->button_memory3(); + } else if (preset == "LTD HT") { + result = this->parent_->button_heat(); + } else if (preset == "EXT HT") { + result = this->parent_->button_ext_heat(); + } else { + ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); + return; + } + + if (result) { + this->custom_preset = preset; + this->preset.reset(); + } + } + + if (call.get_fan_mode().has_value()) { + // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. + // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. + auto fan_mode = *call.get_fan_mode(); + bool result; + if (fan_mode == CLIMATE_FAN_LOW) { + result = this->parent_->set_fan_speed(20); + } else if (fan_mode == CLIMATE_FAN_MEDIUM) { + result = this->parent_->set_fan_speed(50); + } else if (fan_mode == CLIMATE_FAN_HIGH) { + result = this->parent_->set_fan_speed(75); + } else { + ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), + LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); + return; + } + + if (result) { + this->fan_mode = fan_mode; + this->custom_fan_mode.reset(); + } + } else if (call.get_custom_fan_mode().has_value()) { + auto fan_mode = *call.get_custom_fan_mode(); + auto fan_index = bedjet_fan_speed_to_step(fan_mode); + if (fan_index <= 19) { + ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), + fan_index); + bool result = this->parent_->set_fan_index(fan_index); + if (result) { + this->custom_fan_mode = fan_mode; + this->fan_mode.reset(); + } + } + } +} + +void BedJetClimate::on_bedjet_state(bool is_ready) {} + +void BedJetClimate::on_status(const BedjetStatusPacket *data) { + ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data); + + auto converted_temp = bedjet_temp_to_c(data->target_temp_step); + if (converted_temp > 0) + this->target_temperature = converted_temp; + + converted_temp = bedjet_temp_to_c(data->ambient_temp_step); + if (converted_temp > 0) + this->current_temperature = converted_temp; + + const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); + if (fan_mode_name != nullptr) { + this->custom_fan_mode = *fan_mode_name; + } + + // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. + switch (data->mode) { + case MODE_WAIT: // Biorhythm "wait" step: device is idle + case MODE_STANDBY: + this->mode = CLIMATE_MODE_OFF; + this->action = CLIMATE_ACTION_IDLE; + this->fan_mode = CLIMATE_FAN_OFF; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_HEAT: + this->mode = CLIMATE_MODE_HEAT; + this->action = CLIMATE_ACTION_HEATING; + this->preset.reset(); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + this->set_custom_preset_("LTD HT"); + } else { + this->custom_preset.reset(); + } + break; + + case MODE_EXTHT: + this->mode = CLIMATE_MODE_HEAT; + this->action = CLIMATE_ACTION_HEATING; + this->preset.reset(); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + this->custom_preset.reset(); + } else { + this->set_custom_preset_("EXT HT"); + } + break; + + case MODE_COOL: + this->mode = CLIMATE_MODE_FAN_ONLY; + this->action = CLIMATE_ACTION_COOLING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_DRY: + this->mode = CLIMATE_MODE_DRY; + this->action = CLIMATE_ACTION_DRYING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_TURBO: + this->preset = CLIMATE_PRESET_BOOST; + this->custom_preset.reset(); + this->mode = CLIMATE_MODE_HEAT; + this->action = CLIMATE_ACTION_HEATING; + break; + + default: + ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode); + break; + } + + ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(), + LOG_STR_ARG(climate_mode_to_string(this->mode))); + // FIXME: compare new state to previous state. + this->publish_state(); +} + +/** Attempts to update the climate device from the last received BedjetStatusPacket. + * + * This will be called from #on_status() when the parent dispatches new status packets, + * and from #update() when the polling interval is triggered. + * + * @return `true` if the status has been applied; `false` if there is nothing to apply. + */ +bool BedJetClimate::update_status_() { + if (!this->parent_->is_connected()) + return false; + if (!this->parent_->has_status()) + return false; + + auto *status = this->parent_->get_status_packet(); + + if (status == nullptr) + return false; + + this->on_status(status); + + if (this->is_valid_()) { + // TODO: only if state changed? + this->publish_state(); + this->status_clear_warning(); + return true; + } + + return false; +} + +void BedJetClimate::update() { + ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str()); + // TODO: if the hub component is already polling, do we also need to include polling? + // We're already going to get on_status() at the hub's polling interval. + auto result = this->update_status_(); + ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false"); +} + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet_climate.h similarity index 56% rename from esphome/components/bedjet/bedjet.h rename to esphome/components/bedjet/bedjet_climate.h index 5c2930420c..27ee5c7501 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet_climate.h @@ -1,53 +1,34 @@ #pragma once -#include "esphome/components/ble_client/ble_client.h" -#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/climate/climate.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "bedjet_base.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "bedjet_child.h" +#include "bedjet_codec.h" +#include "bedjet_hub.h" #ifdef USE_ESP32 -#include - namespace esphome { namespace bedjet { -namespace espbt = esphome::esp32_ble_tracker; - -static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); -static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); -static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); -static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); - -class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { +class BedJetClimate : public climate::Climate, public BedJetClient, public PollingComponent { public: void setup() override; void loop() override; void update() override; - 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 dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } -#ifdef USE_TIME - void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } - void send_local_time(); -#endif - void set_clock(uint8_t hour, uint8_t minute); - void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } + /* BedJetClient status update */ + void on_status(const BedjetStatusPacket *data) override; + void on_bedjet_state(bool is_ready) override; + std::string describe() override; + /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */ void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; } - /** Attempts to check for and apply firmware updates. */ - void upgrade_firmware(); - climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supports_action(true); @@ -92,20 +73,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod protected: void control(const climate::ClimateCall &call) override; -#ifdef USE_TIME - void setup_time_(); - optional time_id_{}; -#endif - - uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT; - static const uint32_t MIN_NOTIFY_THROTTLE = 5000; - static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; - static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; - - uint8_t set_notify_(bool enable); - uint8_t write_bedjet_packet_(BedjetPacket *pkt); void reset_state_(); bool update_status_(); @@ -114,17 +83,6 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) && this->current_temperature > 1 && this->target_temperature > 1; } - - uint32_t last_notify_ = 0; - bool force_refresh_ = false; - - std::unique_ptr codec_; - uint16_t char_handle_cmd_; - uint16_t char_handle_name_; - uint16_t char_handle_status_; - uint16_t config_descr_status_; - - uint8_t write_notify_config_descriptor_(bool enable); }; } // namespace bedjet diff --git a/esphome/components/bedjet/bedjet_base.cpp b/esphome/components/bedjet/bedjet_codec.cpp similarity index 55% rename from esphome/components/bedjet/bedjet_base.cpp rename to esphome/components/bedjet/bedjet_codec.cpp index 99f1df96d3..735393ffcb 100644 --- a/esphome/components/bedjet/bedjet_base.cpp +++ b/esphome/components/bedjet/bedjet_codec.cpp @@ -1,4 +1,4 @@ -#include "bedjet_base.h" +#include "bedjet_codec.h" #include #include @@ -48,7 +48,16 @@ BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { /** Returns a BedjetPacket that will set the device's current time. */ BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) { - this->packet_.command = CMD_SET_TIME; + this->packet_.command = CMD_SET_CLOCK; + this->packet_.data_length = 2; + this->packet_.data[0] = hour; + this->packet_.data[1] = minute; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's remaining runtime. */ +BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour, const uint8_t minute) { + this->packet_.command = CMD_SET_RUNTIME; this->packet_.data_length = 2; this->packet_.data[0] = hour; this->packet_.data[1] = minute; @@ -57,17 +66,17 @@ BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_ /** Decodes the extra bytes that were received after being notified with a partial packet. */ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { - ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); + ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); uint8_t offset = this->last_buffer_size_; if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { memcpy(((uint8_t *) (&this->buf_)) + offset, data, length); - ESP_LOGV(TAG, - "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " - "flags=BedjetFlags ", - this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase, - this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0', - this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0', - this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01)); + ESP_LOGVV(TAG, + "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " + "flags=BedjetFlags ", + this->buf_.unused_1, this->buf_.unused_2, this->buf_.unused_3, this->buf_.update_phase, + this->buf_.flags.conn_test_passed ? '1' : '0', this->buf_.flags.leds_enabled ? '1' : '0', + this->buf_.flags.units_setup ? '1' : '0', this->buf_.flags.beeps_muted ? '1' : '0', + this->buf_.flags_packed); } else { ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset, sizeof(BedjetStatusPacket), length + offset); @@ -82,8 +91,6 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { - this->status_packet_.reset(); - // Clear old buffer memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); // Copy new data into buffer @@ -91,23 +98,24 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { this->last_buffer_size_ = length; // TODO: validate the packet checksum? - if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && - this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && - this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) { + if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 && + this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && this->buf_.ambient_temp_step > 1 && + this->buf_.ambient_temp_step <= 100) { // and save it for the update() loop - this->status_packet_ = this->buf_; - return this->buf_.is_partial == 1; + this->status_packet_ = &this->buf_; + return this->buf_.is_partial; } else { + this->status_packet_ = nullptr; // TODO: log a warning if we detect that we connected to a non-V3 device. ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length); } } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. - ESP_LOGV(TAG, - "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " - "[12]=%d, [-1]=%d", - bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9], - data[10], data[11], data[12], data[length - 1]); + ESP_LOGVV(TAG, + "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " + "[12]=%d, [-1]=%d", + bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], + data[9], data[10], data[11], data[12], data[length - 1]); if (this->has_status()) { this->status_packet_->ambient_temp_step = data[6]; @@ -119,5 +127,35 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { return false; } +/** @return `true` if the new packet is meaningfully different from the last seen packet. */ +bool BedjetCodec::compare(const uint8_t *data, uint16_t length) { + if (data == nullptr) { + return false; + } + + if (length < 17) { + // New packet looks small, skip it. + return false; + } + + if (this->buf_.packet_format != PACKET_FORMAT_V3_HOME || + this->buf_.packet_type != PACKET_TYPE_STATUS) { // No last seen packet, so take the new one. + return true; + } + + if (data[1] != PACKET_FORMAT_V3_HOME || data[3] != PACKET_TYPE_STATUS) { // New packet is not a v3 status, skip it. + return false; + } + + // Now coerce it to a status packet and compare some key fields + const BedjetStatusPacket *test = reinterpret_cast(data); + // These are fields that will only change due to explicit action. + // That is why we do not check ambient or actual temp here, because those are environmental. + bool explicit_fields_changed = this->buf_.mode != test->mode || this->buf_.fan_step != test->fan_step || + this->buf_.target_temp_step != test->target_temp_step; + + return explicit_fields_changed; +} + } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/bedjet_base.h b/esphome/components/bedjet/bedjet_codec.h similarity index 61% rename from esphome/components/bedjet/bedjet_base.h rename to esphome/components/bedjet/bedjet_codec.h index c63b70cb9a..3a41313ada 100644 --- a/esphome/components/bedjet/bedjet_base.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -14,18 +14,6 @@ struct BedjetPacket { uint8_t data[2]; }; -struct BedjetFlags { - /* uint8_t */ - int a_ : 1; // 0x80 - int b_ : 1; // 0x40 - int conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed. - int leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled. - int c_ : 1; // 0x08 - int units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured. - int d_ : 1; // 0x02 - int beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted. -} __attribute__((packed)); - enum BedjetPacketFormat : uint8_t { PACKET_FORMAT_DEBUG = 0x05, // 5 PACKET_FORMAT_V3_HOME = 0x56, // 86 @@ -36,15 +24,25 @@ enum BedjetPacketType : uint8_t { PACKET_TYPE_DEBUG = 0x2, }; +enum BedjetNotification : uint8_t { + NOTIFY_NONE = 0, ///< No notification pending + NOTIFY_FILTER = 1, ///< Clean Filter / Please check BedJet air filter and clean if necessary. + NOTIFY_UPDATE = 2, ///< Firmware Update / A newer version of firmware is available. + NOTIFY_UPDATE_FAIL = 3, ///< Firmware Update / Unable to connect to the firmware update server. + NOTIFY_BIO_FAIL_CLOCK_NOT_SET = 4, ///< The specified sequence cannot be run because the clock is not set + NOTIFY_BIO_FAIL_TOO_LONG = 5, ///< The specified sequence cannot be run because it contains steps that would be too + ///< long running from the current time. + // Note: after handling a notification, send MAGIC_NOTIFY_ACK +}; + /** The format of a BedJet V3 status packet. */ struct BedjetStatusPacket { // [0] - uint8_t is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the - ///< characteristic. + bool is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the + ///< characteristic. BedjetPacketFormat packet_format : 8; ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets. - uint8_t - expecting_length : 8; ///< The expected total length of the status packet after merging the additional packet. + uint8_t expecting_length : 8; ///< The expected total length of the status packet after merging the extra packet. BedjetPacketType packet_type : 8; ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet. // [4] @@ -77,11 +75,26 @@ struct BedjetStatusPacket { uint8_t shutdown_reason : 8; ///< The reason for the last device shutdown. // [19-25]; the initial partial packet cuts off here after [19] - // Skip 7 bytes? - uint32_t _skip_1_ : 32; // Unknown 19-22 = 0x01810112 - uint16_t _skip_2_ : 16; // Unknown 23-24 = 0x1310 - uint8_t _skip_3_ : 8; // Unknown 25 = 0x00 + uint8_t unused_1 : 8; // Unknown [19] = 0x01 + uint8_t unused_2 : 8; // Unknown [20] = 0x81 + uint8_t unused_3 : 8; // Unknown [21] = 0x01 + + // [22]: 0x2=is_dual_zone, ...? + struct { + int unused_1 : 1; // 0x80 + int unused_2 : 1; // 0x40 + int unused_3 : 1; // 0x20 + int unused_4 : 1; // 0x10 + int unused_5 : 1; // 0x8 + int unused_6 : 1; // 0x4 + bool is_dual_zone : 1; /// Is part of a Dual Zone configuration + int unused_7 : 1; // 0x1 + } dual_zone_flags; + + uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310 + uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310 + uint8_t unused_6 : 8; // Unknown 25 = 0x00 // [26] // 0x18(24) = "Connection test has completed OK" @@ -89,10 +102,27 @@ struct BedjetStatusPacket { uint8_t update_phase : 8; ///< The current status/phase of a firmware update. // [27] - // FIXME: cannot nest packed struct of matching length here? - /* BedjetFlags */ uint8_t flags : 8; /// See BedjetFlags for the packed byte flags. - // [28-31]; 20+11 bytes - uint32_t _skip_4_ : 32; // Unknown + union { + uint8_t flags_packed; + struct { + /* uint8_t */ + int unused_1 : 1; // 0x80 + int unused_2 : 1; // 0x40 + bool conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed. + bool leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled. + int unused_3 : 1; // 0x08 + bool units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured. + int unused_4 : 1; // 0x02 + bool beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted. + } __attribute__((packed)) flags; + }; + + // [28] = (biorhythm?) sequence step + uint8_t bio_sequence_step : 8; /// Biorhythm sequence step number + // [29] = notify_code: + BedjetNotification notify_code : 8; /// See BedjetNotification + + uint16_t unused_7 : 16; // Unknown } __attribute__((packed)); @@ -127,7 +157,7 @@ struct BedjetStatusPacket { * - Set current time * The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might * contain time-of-day based step rules. - * - BedjetPacket#command = BedjetCommand::CMD_SET_TIME + * - BedjetPacket#command = BedjetCommand::CMD_SET_CLOCK * - BedjetPacket#data [0] is hours, [1] is minutes */ class BedjetCodec { @@ -136,13 +166,15 @@ class BedjetCodec { BedjetPacket *get_set_target_temp_request(float temperature); BedjetPacket *get_set_fan_speed_request(uint8_t fan_step); BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute); + BedjetPacket *get_set_runtime_remaining_request(uint8_t hour, uint8_t minute); bool decode_notify(const uint8_t *data, uint16_t length); void decode_extra(const uint8_t *data, uint16_t length); + bool compare(const uint8_t *data, uint16_t length); - inline bool has_status() { return this->status_packet_.has_value(); } - const optional &get_status_packet() const { return this->status_packet_; } - void clear_status() { this->status_packet_.reset(); } + inline bool has_status() { return this->status_packet_ != nullptr; } + const BedjetStatusPacket *get_status_packet() const { return this->status_packet_; } + void clear_status() { this->status_packet_ = nullptr; } protected: BedjetPacket *clean_packet_(); @@ -151,7 +183,7 @@ class BedjetCodec { BedjetPacket packet_; - optional status_packet_; + BedjetStatusPacket *status_packet_; BedjetStatusPacket buf_; }; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 16f73717c6..bd2fb2421d 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -7,6 +7,14 @@ namespace bedjet { static const char *const TAG = "bedjet"; +/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. +inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { + // 0 = 5% + // 19 = 100% + return 5 * fan + 5; +} +inline static uint8_t bedjet_fan_speed_to_index(const uint8_t speed) { return speed / 5 - 1; } + enum BedjetMode : uint8_t { /// BedJet is Off MODE_STANDBY = 0, @@ -62,14 +70,17 @@ enum BedjetButton : uint8_t { MAGIC_CONNTEST = 0x42, /// Request a firmware update. This will also restart the Bedjet. MAGIC_UPDATE = 0x43, + /// Acknowledge notification handled. See BedjetNotify + MAGIC_NOTIFY_ACK = 0x52, }; enum BedjetCommand : uint8_t { CMD_BUTTON = 0x1, + CMD_SET_RUNTIME = 0x2, CMD_SET_TEMP = 0x3, CMD_STATUS = 0x6, CMD_SET_FAN = 0x7, - CMD_SET_TIME = 0x8, + CMD_SET_CLOCK = 0x8, }; #define BEDJET_FAN_STEP_NAMES_ \ diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp new file mode 100644 index 0000000000..fd383eb6be --- /dev/null +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -0,0 +1,559 @@ +#include "bedjet_hub.h" +#include "bedjet_child.h" +#include "bedjet_const.h" + +namespace esphome { +namespace bedjet { + +static const LogString *bedjet_button_to_string(BedjetButton button) { + switch (button) { + case BTN_OFF: + return LOG_STR("OFF"); + case BTN_COOL: + return LOG_STR("COOL"); + case BTN_HEAT: + return LOG_STR("HEAT"); + case BTN_EXTHT: + return LOG_STR("EXT HT"); + case BTN_TURBO: + return LOG_STR("TURBO"); + case BTN_DRY: + return LOG_STR("DRY"); + case BTN_M1: + return LOG_STR("M1"); + case BTN_M2: + return LOG_STR("M2"); + case BTN_M3: + return LOG_STR("M3"); + default: + return LOG_STR("unknown"); + } +} + +/* Public */ + +void BedJetHub::upgrade_firmware() { + auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name().c_str(), status); + } +} + +bool BedJetHub::button_heat() { return this->send_button(BTN_HEAT); } +bool BedJetHub::button_ext_heat() { return this->send_button(BTN_EXTHT); } +bool BedJetHub::button_turbo() { return this->send_button(BTN_TURBO); } +bool BedJetHub::button_cool() { return this->send_button(BTN_COOL); } +bool BedJetHub::button_dry() { return this->send_button(BTN_DRY); } +bool BedJetHub::button_off() { return this->send_button(BTN_OFF); } +bool BedJetHub::button_memory1() { return this->send_button(BTN_M1); } +bool BedJetHub::button_memory2() { return this->send_button(BTN_M2); } +bool BedJetHub::button_memory3() { return this->send_button(BTN_M3); } + +bool BedJetHub::set_fan_index(uint8_t fan_speed_index) { + if (fan_speed_index > 19) { + ESP_LOGW(TAG, "Invalid fan speed index %d, expecting 0-19.", fan_speed_index); + return false; + } + + auto *pkt = this->codec_->get_set_fan_speed_request(fan_speed_index); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name().c_str(), status); + } + return status == 0; +} + +uint8_t BedJetHub::get_fan_index() { + auto *status = this->codec_->get_status_packet(); + if (status != nullptr) { + return status->fan_step; + } + return 0; +} + +bool BedJetHub::set_target_temp(float temp_c) { + auto *pkt = this->codec_->get_set_target_temp_request(temp_c); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name().c_str(), status); + } + return status == 0; +} + +bool BedJetHub::set_time_remaining(uint8_t hours, uint8_t mins) { + // FIXME: this may fail depending on current mode or other restrictions enforced by the unit. + auto *pkt = this->codec_->get_set_runtime_remaining_request(hours, mins); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name().c_str(), status); + } + return status == 0; +} + +bool BedJetHub::send_button(BedjetButton button) { + auto *pkt = this->codec_->get_button_request(button); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name().c_str(), + LOG_STR_ARG(bedjet_button_to_string(button)), status); + } else { + ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(), + LOG_STR_ARG(bedjet_button_to_string(button))); + } + return status == 0; +} + +uint16_t BedJetHub::get_time_remaining() { + auto *status = this->codec_->get_status_packet(); + if (status != nullptr) { + return status->time_remaining_secs + status->time_remaining_mins * 60 + status->time_remaining_hrs * 3600; + } + return 0; +} + +/* Bluetooth/GATT */ + +uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) { + if (!this->is_connected()) { + if (!this->parent_->enabled) { + ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); + } else { + ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); + } + return -1; + } + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, + pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + return status; +} + +/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ +uint8_t BedJetHub::set_notify_(const bool enable) { + uint8_t status; + if (enable) { + status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + this->char_handle_status_); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + } else { + status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + this->char_handle_status_); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); + } + } + ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); + return status; +} + +bool BedJetHub::discover_characteristics_() { + bool result = true; + esphome::ble_client::BLECharacteristic *chr; + + if (!this->char_handle_cmd_) { + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); + result = false; + } else { + this->char_handle_cmd_ = chr->handle; + } + } + + if (!this->char_handle_status_) { + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); + result = false; + } else { + this->char_handle_status_ = chr->handle; + } + } + + if (!this->config_descr_status_) { + // We also need to obtain the config descriptor for this handle. + // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be + // able to look it up. + auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); + if (descr == nullptr) { + ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", + this->char_handle_status_); + result = false; + } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, + descr->uuid.to_string().c_str()); + result = false; + } else { + this->config_descr_status_ = descr->handle; + } + } + + if (!this->char_handle_name_) { + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name().c_str()); + result = false; + } else { + this->char_handle_name_ = chr->handle; + auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); + } + } + } + + ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str()); + ESP_LOGI(TAG, " - Command char: 0x%x", this->char_handle_cmd_); + ESP_LOGI(TAG, " - Status char: 0x%x", this->char_handle_status_); + ESP_LOGI(TAG, " - config descriptor: 0x%x", this->config_descr_status_); + ESP_LOGI(TAG, " - Name char: 0x%x", this->char_handle_name_); + + return result; +} + +void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); + this->status_set_warning(); + this->dispatch_state_(false); + break; + } + case ESP_GATTC_OPEN_EVT: { + // FIXME: bug in BLEClient + this->parent_->conn_id = param->open.conn_id; + this->open_conn_id_ = param->open.conn_id; + break; + } + + case ESP_GATTC_CONNECT_EVT: { + if (this->parent_->conn_id != param->connect.conn_id && this->open_conn_id_ != 0xff) { + // FIXME: bug in BLEClient + ESP_LOGW(TAG, "[%s] CONNECT_EVT unexpected conn_id; open=%d, parent=%d, param=%d", this->get_name().c_str(), + this->open_conn_id_, this->parent_->conn_id, param->connect.conn_id); + this->parent_->conn_id = this->open_conn_id_; + } + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto result = this->discover_characteristics_(); + + if (result) { + ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name().c_str()); + this->node_state = espbt::ClientState::ESTABLISHED; + this->set_notify_(true); + +#ifdef USE_TIME + if (this->time_id_.has_value()) { + this->send_local_time(); + } +#endif + + this->dispatch_state_(true); + } else { + ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name().c_str()); + this->parent()->set_enabled(false); + this->status_set_warning(); + this->dispatch_state_(false); + } + break; + } + case ESP_GATTC_WRITE_DESCR_EVT: { + if (param->write.status != ESP_GATT_OK) { + if (param->write.status == ESP_GATT_INVALID_ATTR_LEN) { + // This probably means that our hack for notify_en (8 bit vs 16 bit) didn't work right. + // Should we try to fall back to BLEClient's way? + ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name().c_str(), + param->write.handle, param->write.status); + } else { + ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(), + param->write.handle, param->write.status); + } + break; + } + ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, + param->write.status); + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: { + if (param->write.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); + break; + } + if (param->write.handle == this->char_handle_cmd_) { + if (this->force_refresh_) { + // Command write was successful. Publish the pending state, hoping that notify will kick in. + // FIXME: better to wait until we know the status has changed + this->dispatch_status_(); + } + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent_->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + + if (param->read.handle == this->char_handle_status_) { + // This is the additional packet that doesn't fit in the notify packet. + this->codec_->decode_extra(param->read.value, param->read.value_len); + this->status_packet_ready_(); + } else if (param->read.handle == this->char_handle_name_) { + // The data should represent the name. + if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { + std::string bedjet_name(reinterpret_cast(param->read.value), param->read.value_len); + ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); + this->set_name_(bedjet_name); + } + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + // This event means that ESP received the request to enable notifications on the client side. But we also have to + // tell the server that we want it to send notifications. Normally BLEClient parent would handle this + // automatically, but as soon as we set our status to Established, the parent is going to purge all the + // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable + // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write + // doesn't break anything. + + if (param->reg_for_notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", + this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); + break; + } + + this->write_notify_config_descriptor_(true); + this->last_notify_ = 0; + this->force_refresh_ = true; + break; + } + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { + // This event is not handled by the parent BLEClient, so we need to do this either way. + if (param->unreg_for_notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", + this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); + break; + } + + this->write_notify_config_descriptor_(false); + this->last_notify_ = 0; + // Now we wait until the next update() poll to re-register notify... + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (this->processing_) + break; + + if (param->notify.conn_id != this->parent_->conn_id) { + ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x", + this->get_name().c_str(), this->parent_->conn_id, param->notify.conn_id); + // FIXME: bug in BLEClient holding wrong conn_id. + } + + if (param->notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), + this->char_handle_status_, param->notify.handle); + break; + } + + // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we + // throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). + // Another idea would be to keep notify off by default, and use update() as an opportunity to turn on + // notify to get enough data to update status, then turn off notify again. + + uint32_t now = millis(); + auto delta = now - this->last_notify_; + + if (!this->force_refresh_ && this->codec_->compare(param->notify.value, param->notify.value_len)) { + // If the packet is meaningfully different, trigger children as well + this->force_refresh_ = true; + ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name().c_str()); + } + + if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { + // Set reentrant flag to prevent processing multiple packets. + this->processing_ = true; + ESP_LOGVV(TAG, "[%s] Decoding packet: last=%d, delta=%d, force=%s", this->get_name().c_str(), + this->last_notify_, delta, this->force_refresh_ ? "y" : "n"); + bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); + + if (needs_extra) { + // This means the packet was partial, so read the status characteristic to get the second part. + // Ideally this will complete quickly. We won't process additional notification events until it does. + auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, + this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); + } + } else { + this->status_packet_ready_(); + } + } + break; + } + default: + ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); + break; + } +} + +inline void BedJetHub::status_packet_ready_() { + this->last_notify_ = millis(); + this->processing_ = false; + + if (this->force_refresh_) { + // If we requested an immediate update, do that now. + this->update(); + this->force_refresh_ = false; + } +} + +/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. + * + * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order + * to undo the same on unregister. It also allows us to maintain the config descriptor separately, + * since the parent BLEClient is going to purge all descriptors once we set our connection status + * to `Established`. + */ +uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { + auto handle = this->config_descr_status_; + if (handle == 0) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); + return -1; + } + + // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. + uint16_t notify_en = enable ? 1 : 0; + auto status = + esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), + (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + return status; + } + ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(), + enable ? "true" : "false", handle, this->parent_->conn_id); + return ESP_GATT_OK; +} + +/* Time Component */ + +#ifdef USE_TIME +void BedJetHub::send_local_time() { + if (this->time_id_.has_value()) { + auto *time_id = *this->time_id_; + time::ESPTime now = time_id->now(); + if (now.is_valid()) { + this->set_clock(now.hour, now.minute); + ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); + } + } else { + ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); + } +} + +void BedJetHub::setup_time_() { + if (this->time_id_.has_value()) { + this->send_local_time(); + auto *time_id = *this->time_id_; + time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); + } else { + ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); + } +} +#endif + +void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { + if (!this->is_connected()) { + ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); + return; + } + + BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); + } else { + ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); + } +} + +/* Internal */ + +void BedJetHub::loop() {} +void BedJetHub::update() { this->dispatch_status_(); } + +void BedJetHub::dump_config() { + ESP_LOGCONFIG(TAG, "BedJet Hub '%s'", this->get_name().c_str()); + ESP_LOGCONFIG(TAG, " ble_client.app_id: %d", this->parent()->app_id); + ESP_LOGCONFIG(TAG, " ble_client.conn_id: %d", this->parent()->conn_id); + LOG_UPDATE_INTERVAL(this) + ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size()); + for (auto *child : this->children_) { + ESP_LOGCONFIG(TAG, " - %s", child->describe().c_str()); + } +} + +void BedJetHub::dispatch_state_(bool is_ready) { + for (auto *child : this->children_) { + child->on_bedjet_state(is_ready); + } +} + +void BedJetHub::dispatch_status_() { + auto *status = this->codec_->get_status_packet(); + + if (!this->is_connected()) { + ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name().c_str()); + } else if (status != nullptr) { + ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(), + status); + for (auto *child : this->children_) { + child->on_status(status); + } + } else { + uint32_t now = millis(); + uint32_t diff = now - this->last_notify_; + + if (this->last_notify_ == 0) { + // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. + // However, it could also mean that it's running, but failing to send notifications. + // We can try to unregister for notifications now, and then re-register, hoping to clear it up... + // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? + + ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); + } else if (diff > NOTIFY_WARN_THRESHOLD) { + ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); + } + + if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { + ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); + // set_enabled(false) will only close the connection if state != IDLE. + this->parent()->set_state(espbt::ClientState::CONNECTING); + this->parent()->set_enabled(false); + this->parent()->set_enabled(true); + } + } +} + +void BedJetHub::register_child(BedJetClient *obj) { + this->children_.push_back(obj); + obj->set_parent(this); +} + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h new file mode 100644 index 0000000000..c7583b78e9 --- /dev/null +++ b/esphome/components/bedjet/bedjet_hub.h @@ -0,0 +1,178 @@ +#pragma once + +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "bedjet_child.h" +#include "bedjet_codec.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace bedjet { + +namespace espbt = esphome::esp32_ble_tracker; + +// Forward declare BedJetClient +class BedJetClient; + +static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); + +/** + * Hub component connecting to the BedJet device over Bluetooth. + */ +class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + /* BedJet functionality exposed to `BedJetClient` children and/or accessible from action lambdas. */ + + /** Attempts to check for and apply firmware updates. */ + void upgrade_firmware(); + + /** Press the OFF button. */ + bool button_off(); + /** Press the HEAT button. */ + bool button_heat(); + /** Press the EXT HT button. */ + bool button_ext_heat(); + /** Press the TURBO button. */ + bool button_turbo(); + /** Press the COOL button. */ + bool button_cool(); + /** Press the DRY button. */ + bool button_dry(); + /** Press the M1 (memory recall) button. */ + bool button_memory1(); + /** Press the M2 (memory recall) button. */ + bool button_memory2(); + /** Press the M3 (memory recall) button. */ + bool button_memory3(); + + /** Send the `button`. */ + bool send_button(BedjetButton button); + + /** Set the target temperature to `temp_c` in °C. */ + bool set_target_temp(float temp_c); + + /** Set the fan speed to a stepped index in the range 0-19. */ + bool set_fan_index(uint8_t fan_speed_index); + + /** Set the fan speed to a percent in the range 5% - 100%, at 5% increments. */ + bool set_fan_speed(uint8_t fan_speed_pct) { return this->set_fan_index(bedjet_fan_speed_to_index(fan_speed_pct)); } + + /** Return the fan speed index, in the range 0-19. */ + uint8_t get_fan_index(); + + /** Return the fan speed as a percent in the range 5%-100%. */ + uint8_t get_fan_speed() { return bedjet_fan_step_to_speed(this->get_fan_index()); } + + /** Set the operational runtime remaining. + * + * The unit establishes and enforces runtime limits for some modes, so this call is not guaranteed to succeed. + */ + bool set_time_remaining(uint8_t hours, uint8_t mins); + + /** Return the remaining runtime, in seconds. */ + uint16_t get_time_remaining(); + + /** @return `true` if the `BLEClient::node_state` is `ClientState::ESTABLISHED`. */ + bool is_connected() { return this->node_state == espbt::ClientState::ESTABLISHED; } + + bool has_status() { return this->codec_->has_status(); } + const BedjetStatusPacket *get_status_packet() const { return this->codec_->get_status_packet(); } + + /** Register a `BedJetClient` child component. */ + void register_child(BedJetClient *obj); + + /** Set the status timeout. + * + * This is the max time to wait for a status update before the connection is presumed unusable. + */ + void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } + +#ifdef USE_TIME + /** Set the `time::RealTimeClock` implementation. */ + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } + /** Attempts to sync the local time (via `time_id`) to the BedJet device. */ + void send_local_time(); +#endif + /** Attempt to set the BedJet device's clock to the specified time. */ + void set_clock(uint8_t hour, uint8_t minute); + + /* Component overrides */ + + void loop() override; + void update() override; + void dump_config() override; + void setup() override { this->codec_ = make_unique(); } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + /** @return The BedJet's configured name, or the MAC address if not discovered yet. */ + std::string get_name() { + if (this->name_.empty()) { + return this->parent_->address_str(); + } else { + return this->name_; + } + } + + /* BLEClient overrides */ + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + protected: + std::vector children_; + void dispatch_status_(); + void dispatch_state_(bool is_ready); + +#ifdef USE_TIME + /** Initializes time sync callbacks to support syncing current time to the BedJet. */ + void setup_time_(); + optional time_id_{}; +#endif + + uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; + static const uint32_t MIN_NOTIFY_THROTTLE = 15000; + static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; + static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; + + uint8_t set_notify_(bool enable); + /** Send the `BedjetPacket` to the device. */ + uint8_t write_bedjet_packet_(BedjetPacket *pkt); + void set_name_(const std::string &name) { this->name_ = name; } + + std::string name_; + + uint32_t last_notify_ = 0; + inline void status_packet_ready_(); + bool force_refresh_ = false; + bool processing_ = false; + + std::unique_ptr codec_; + + bool discover_characteristics_(); + uint16_t char_handle_cmd_; + uint16_t char_handle_name_; + uint16_t char_handle_status_; + uint16_t config_descr_status_; + + uint8_t open_conn_id_ = -1; + + uint8_t write_notify_config_descriptor_(bool enable); +}; + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/climate.py b/esphome/components/bedjet/climate.py index d718ba9969..9865cd716d 100644 --- a/esphome/components/bedjet/climate.py +++ b/esphome/components/bedjet/climate.py @@ -1,19 +1,26 @@ +import logging + import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, ble_client, time +from esphome.components import climate, ble_client from esphome.const import ( CONF_HEAT_MODE, CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TIME_ID, ) +from . import ( + BEDJET_CLIENT_SCHEMA, + register_bedjet_child, +) +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["ble_client"] bedjet_ns = cg.esphome_ns.namespace("bedjet") -Bedjet = bedjet_ns.class_( - "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent +BedJetClimate = bedjet_ns.class_( + "BedJetClimate", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent ) BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") BEDJET_HEAT_MODES = { @@ -24,18 +31,30 @@ BEDJET_HEAT_MODES = { CONFIG_SCHEMA = ( climate.CLIMATE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(Bedjet), + cv.GenerateID(): cv.declare_id(BedJetClimate), cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( BEDJET_HEAT_MODES, lower=True ), - cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Optional( - CONF_RECEIVE_TIMEOUT, default="0s" - ): cv.positive_time_period_milliseconds, } ) - .extend(ble_client.BLE_CLIENT_SCHEMA) - .extend(cv.polling_component_schema("30s")) + .extend(cv.polling_component_schema("60s")) + .extend( + # TODO: remove compat layer. + { + cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid( + "The 'ble_client_id' option has been removed. Please migrate " + "to the new `bedjet_id` option in the `bedjet` component.\n" + "See https://esphome.io/components/climate/bedjet.html" + ), + cv.Optional(CONF_TIME_ID): cv.invalid( + "The 'time_id' option has been moved to the `bedjet` component." + ), + cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid( + "The 'receive_timeout' option has been moved to the `bedjet` component." + ), + } + ) + .extend(BEDJET_CLIENT_SCHEMA) ) @@ -43,10 +62,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await climate.register_climate(var, config) - await ble_client.register_ble_node(var, config) + await register_bedjet_child(var, config) + cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) - if CONF_TIME_ID in config: - time_ = await cg.get_variable(config[CONF_TIME_ID]) - cg.add(var.set_time_id(time_)) - if CONF_RECEIVE_TIMEOUT in config: - cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 63fd34c7d5..600c9efe5a 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, + CONF_PUBLISH_INITIAL_STATE, CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, @@ -328,6 +329,7 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTBinarySensorComponent ), + cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation( @@ -420,6 +422,8 @@ async def setup_binary_sensor_core_(var, config): if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + if CONF_PUBLISH_INITIAL_STATE in config: + cg.add(var.set_publish_initial_state(config[CONF_PUBLISH_INITIAL_STATE])) if CONF_INVERTED in config: cg.add(var.set_inverted(config[CONF_INVERTED])) if CONF_FILTERS in config: @@ -479,8 +483,8 @@ async def register_binary_sensor(var, config): await setup_binary_sensor_core_(var, config) -async def new_binary_sensor(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_binary_sensor(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_binary_sensor(var, config) return var diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 07217eebed..1f837e3ac1 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -37,7 +37,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) { } this->has_state_ = true; this->state = state; - if (!is_initial) { + if (!is_initial || this->publish_initial_state_) { this->state_callback_.call(state); } } diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 87d87b8eb4..b0c39a2c9b 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -58,6 +58,8 @@ class BinarySensor : public EntityBase { void add_filter(Filter *filter); void add_filters(const std::vector &filters); + void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; } + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void send_state_internal(bool state, bool is_initial); @@ -80,6 +82,7 @@ class BinarySensor : public EntityBase { optional device_class_{}; ///< Stores the override of the device class Filter *filter_list_{nullptr}; bool has_state_{false}; + bool publish_initial_state_{false}; Deduplicator publish_dedup_; }; diff --git a/esphome/components/bl0939/bl0939.cpp b/esphome/components/bl0939/bl0939.cpp index 61d7835a4b..0575507c46 100644 --- a/esphome/components/bl0939/bl0939.cpp +++ b/esphome/components/bl0939/bl0939.cpp @@ -7,7 +7,7 @@ namespace bl0939 { static const char *const TAG = "bl0939"; // https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf -// (unfortunatelly chinese, but the protocol can be understood with some translation tool) +// (unfortunately chinese, but the protocol can be understood with some translation tool) static const uint8_t BL0939_READ_COMMAND = 0x55; // 0x5{A4,A3,A2,A1} static const uint8_t BL0939_FULL_PACKET = 0xAA; static const uint8_t BL0939_PACKET_HEADER = 0x55; diff --git a/esphome/components/bl0939/bl0939.h b/esphome/components/bl0939/bl0939.h index 5221ae26e7..d3dab67cc1 100644 --- a/esphome/components/bl0939/bl0939.h +++ b/esphome/components/bl0939/bl0939.h @@ -8,7 +8,7 @@ namespace esphome { namespace bl0939 { // https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf -// (unfortunatelly chinese, but the formulas can be easily understood) +// (unfortunately chinese, but the formulas can be easily understood) // Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm) // and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm) // as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 4bd5c25246..4b7d5f5b8a 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -2,12 +2,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import esp32_ble_tracker from esphome.const import ( + CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_MAC_ADDRESS, CONF_NAME, CONF_ON_CONNECT, CONF_ON_DISCONNECT, + CONF_SERVICE_UUID, CONF_TRIGGER_ID, + CONF_VALUE, ) from esphome import automation @@ -27,6 +30,8 @@ BLEClientConnectTrigger = ble_client_ns.class_( BLEClientDisconnectTrigger = ble_client_ns.class_( "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) ) +# Actions +BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) # Espressif platformio framework is built with MAX_BLE_CONN to 3, so # enforce this in yaml checks. @@ -72,6 +77,39 @@ async def register_ble_node(var, config): cg.add(parent.register_ble_node(var)) +BLE_WRITE_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLEClient), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_VALUE): cv.templatable(cv.ensure_list(cv.hex_uint8_t)), + } +) + + +@automation.register_action( + "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA +) +async def ble_write_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + value = config[CONF_VALUE] + if cg.is_template(value): + templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_value_template(templ)) + else: + cg.add(var.set_value_simple(value)) + + serv_uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(serv_uuid128)) + char_uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(char_uuid128)) + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp new file mode 100644 index 0000000000..6918ab31b4 --- /dev/null +++ b/esphome/components/ble_client/automation.cpp @@ -0,0 +1,75 @@ +#include "automation.h" + +#include +#include +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace ble_client { +static const char *const TAG = "ble_client.automation"; + +void BLEWriterClientNode::write(const std::vector &value) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected"); + return; + } else if (this->ble_char_handle_ == 0) { + ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found"); + return; + } + esp_gatt_write_type_t write_type; + if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { + write_type = ESP_GATT_WRITE_TYPE_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); + } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { + write_type = ESP_GATT_WRITE_TYPE_NO_RSP; + ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); + } else { + ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str()); + return; + } + ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->gattc_if, this->parent()->conn_id, this->ble_char_handle_, value.size(), + const_cast(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); + } +} + +void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str()); + break; + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s", + this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str()); + break; + } + this->ble_char_handle_ = chr->handle; + this->char_props_ = chr->properties; + this->node_state = espbt::ClientState::ESTABLISHED; + ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), + ble_client_->address_str().c_str()); + break; + } + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::IDLE; + this->ble_char_handle_ = 0; + ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str()); + break; + default: + break; + } +} + +} // namespace ble_client +} // namespace esphome diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 6c374046ba..38e64ebd76 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" @@ -13,10 +15,10 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { - if (event == ESP_GATTC_OPEN_EVT && param->open.status == ESP_GATT_OK) - this->trigger(); - if (event == ESP_GATTC_SEARCH_CMPL_EVT) + if (event == ESP_GATTC_SEARCH_CMPL_EVT) { this->node_state = espbt::ClientState::ESTABLISHED; + this->trigger(); + } } }; @@ -33,6 +35,59 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { } }; +class BLEWriterClientNode : public BLEClientNode { + public: + BLEWriterClientNode(BLEClient *ble_client) { + ble_client->register_ble_node(this); + ble_client_ = ble_client; + } + + // Attempts to write the contents of value to char_uuid_. + void write(const std::vector &value); + + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + private: + BLEClient *ble_client_; + int ble_char_handle_ = 0; + esp_gatt_char_prop_t char_props_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; +}; + +template class BLEClientWriteAction : public Action, public BLEWriterClientNode { + public: + BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {} + + void play(Ts... x) override { + if (has_simple_value_) { + return write(this->value_simple_); + } else { + return write(this->value_template_(x...)); + } + } + + void set_value_template(std::function(Ts...)> func) { + this->value_template_ = std::move(func); + has_simple_value_ = false; + } + + void set_value_simple(const std::vector &value) { + this->value_simple_ = value; + has_simple_value_ = true; + } + + private: + bool has_simple_value_ = true; + std::vector value_simple_; + std::function(Ts...)> value_template_{}; +}; + } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index d06c3c4cad..5f58d8273f 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -54,6 +54,7 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { this->remote_bda[3] = (addr >> 16) & 0xFF; this->remote_bda[4] = (addr >> 8) & 0xFF; this->remote_bda[5] = (addr >> 0) & 0xFF; + this->remote_addr_type = device.get_address_type(); return true; } @@ -83,7 +84,7 @@ void BLEClient::set_enabled(bool enabled) { void BLEClient::connect() { ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); - auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, BLE_ADDR_TYPE_PUBLIC, true); + auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, this->remote_addr_type, true); if (ret) { ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); this->set_states_(espbt::ClientState::IDLE); diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index b122bfd11e..5ed8f219d1 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -115,6 +115,7 @@ class BLEClient : public espbt::ESPBTClient, public Component { int gattc_if; esp_bd_addr_t remote_bda; + esp_ble_addr_type_t remote_addr_type; uint16_t conn_id; uint64_t address; bool enabled; diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py index e28421d1a7..fd847d80b8 100644 --- a/esphome/components/ble_client/output/__init__.py +++ b/esphome/components/ble_client/output/__init__.py @@ -1,13 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import ble_client, esp32_ble_tracker, output -from esphome.const import CONF_ID, CONF_SERVICE_UUID +from esphome.const import CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID from .. import ble_client_ns DEPENDENCIES = ["ble_client"] -CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_REQUIRE_RESPONSE = "require_response" BLEBinaryOutput = ble_client_ns.class_( diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index 71cfb03ae0..e8f84d2542 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( + CONF_CHARACTERISTIC_UUID, CONF_LAMBDA, CONF_TRIGGER_ID, CONF_SERVICE_UUID, @@ -11,7 +12,6 @@ from .. import ble_client_ns DEPENDENCIES = ["ble_client"] -CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_NOTIFY = "notify" diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index e1f97e4a01..66f00c551b 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor, ble_client, esp32_ble_tracker from esphome.const import ( + CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_TRIGGER_ID, CONF_SERVICE_UUID, @@ -11,7 +12,6 @@ from .. import ble_client_ns DEPENDENCIES = ["ble_client"] -CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_DESCRIPTOR_UUID = "descriptor_uuid" CONF_NOTIFY = "notify" diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index a4ea8d608e..345b24a36e 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -1,4 +1,5 @@ #include "bme280.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -28,6 +29,7 @@ static const uint8_t BME280_REGISTER_DIG_H5 = 0xE5; static const uint8_t BME280_REGISTER_DIG_H6 = 0xE7; static const uint8_t BME280_REGISTER_CHIPID = 0xD0; +static const uint8_t BME280_REGISTER_RESET = 0xE0; static const uint8_t BME280_REGISTER_CONTROLHUMID = 0xF2; static const uint8_t BME280_REGISTER_STATUS = 0xF3; @@ -39,6 +41,8 @@ static const uint8_t BME280_REGISTER_TEMPDATA = 0xFA; static const uint8_t BME280_REGISTER_HUMIDDATA = 0xFD; static const uint8_t BME280_MODE_FORCED = 0b01; +static const uint8_t BME280_SOFT_RESET = 0xB6; +static const uint8_t BME280_STATUS_IM_UPDATE = 0b01; inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } @@ -97,6 +101,28 @@ void BME280Component::setup() { return; } + // Send a soft reset. + if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { + this->mark_failed(); + return; + } + // Wait until the NVM data has finished loading. + uint8_t status; + uint8_t retry = 5; + do { + delay(2); + if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { + ESP_LOGW(TAG, "Error reading status register."); + this->mark_failed(); + return; + } + } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); + if (status & BME280_STATUS_IM_UPDATE) { + ESP_LOGW(TAG, "Timeout loading NVM."); + this->mark_failed(); + return; + } + // Read calibration this->calibration_.t1 = read_u16_le_(BME280_REGISTER_DIG_T1); this->calibration_.t2 = read_s16_le_(BME280_REGISTER_DIG_T2); diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 1e248ddf07..b0611a62e9 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -102,8 +102,8 @@ async def register_button(var, config): await setup_button_core_(var, config) -async def new_button(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_button(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_button(var, config) return var diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 16c9cd05be..38ded6cdf7 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -1,19 +1,10 @@ #include "climate_traits.h" -#include namespace esphome { namespace climate { int8_t ClimateTraits::get_temperature_accuracy_decimals() const { - // use printf %g to find number of digits based on temperature step - char buf[32]; - sprintf(buf, "%.5g", this->visual_temperature_step_); - std::string str{buf}; - size_t dot_pos = str.find('.'); - if (dot_pos == std::string::npos) - return 0; - - return str.length() - dot_pos - 1; + return step_to_accuracy_decimals(this->visual_temperature_step_); } } // namespace climate diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 9f0a59377d..4a52770bcf 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -131,7 +131,7 @@ void CurrentBasedCover::dump_config() { ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f); ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_)); diff --git a/esphome/components/dac7678/__init__.py b/esphome/components/dac7678/__init__.py new file mode 100644 index 0000000000..b6cd2b384e --- /dev/null +++ b/esphome/components/dac7678/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@NickB1"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +dac7678_ns = cg.esphome_ns.namespace("dac7678") +DAC7678Output = dac7678_ns.class_("DAC7678Output", cg.Component, i2c.I2CDevice) +CONF_INTERNAL_REFERENCE = "internal_reference" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DAC7678Output), + cv.Optional(CONF_INTERNAL_REFERENCE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_internal_reference(config[CONF_INTERNAL_REFERENCE])) + await i2c.register_i2c_device(var, config) + return var diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp new file mode 100644 index 0000000000..bfb18e4a4e --- /dev/null +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -0,0 +1,83 @@ +#include "dac7678_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace dac7678 { + +static const char *const TAG = "dac7678"; + +static const uint8_t DAC7678_REG_INPUT_N = 0x00; +static const uint8_t DAC7678_REG_SELECT_UPDATE_N = 0x10; +static const uint8_t DAC7678_REG_WRITE_N_UPDATE_ALL = 0x20; +static const uint8_t DAC7678_REG_WRITE_N_UPDATE_N = 0x30; +static const uint8_t DAC7678_REG_POWER = 0x40; +static const uint8_t DAC7678_REG_CLEAR_CODE = 0x50; +static const uint8_t DAC7678_REG_LDAC = 0x60; +static const uint8_t DAC7678_REG_SOFTWARE_RESET = 0x70; +static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80; +static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90; + +void DAC7678Output::setup() { + ESP_LOGCONFIG(TAG, "Setting up DAC7678OutputComponent..."); + + ESP_LOGV(TAG, "Resetting device..."); + + // Reset device + if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) { + ESP_LOGE(TAG, "Reset failed"); + this->mark_failed(); + return; + } else + ESP_LOGV(TAG, "Reset succeeded"); + + delayMicroseconds(1000); + + // Set internal reference + if (this->internal_reference_) { + if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) { + ESP_LOGE(TAG, "Set internal reference failed"); + this->mark_failed(); + return; + } else + ESP_LOGV(TAG, "Internal reference enabled"); + } +} + +void DAC7678Output::dump_config() { + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up DAC7678 failed!"); + } else + ESP_LOGCONFIG(TAG, "DAC7678 initialised"); +} + +void DAC7678Output::register_channel(DAC7678Channel *channel) { + auto c = channel->channel_; + this->min_channel_ = std::min(this->min_channel_, c); + this->max_channel_ = std::max(this->max_channel_, c); + channel->set_parent(this); + ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_); +} + +void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->dac_input_reg_[channel] != value) { + ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); + + if (!this->write_byte_16(DAC7678_REG_WRITE_N_UPDATE_N | channel, value << 4)) { + this->status_set_warning(); + return; + } + } + this->dac_input_reg_[channel] = value; + this->status_clear_warning(); +} + +void DAC7678Channel::write_state(float state) { + const float input_rounded = roundf(state * this->full_scale_); + auto input = static_cast(input_rounded); + this->parent_->set_channel_value_(this->channel_, input); +} + +} // namespace dac7678 +} // namespace esphome diff --git a/esphome/components/dac7678/dac7678_output.h b/esphome/components/dac7678/dac7678_output.h new file mode 100644 index 0000000000..abd9875e4c --- /dev/null +++ b/esphome/components/dac7678/dac7678_output.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace dac7678 { + +class DAC7678Output; + +class DAC7678Channel : public output::FloatOutput, public Parented { + public: + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + friend class DAC7678Output; + + const uint16_t full_scale_ = 0xFFF; + + void write_state(float state) override; + + uint8_t channel_; +}; + +/// DAC7678 float output component. +class DAC7678Output : public Component, public i2c::I2CDevice { + public: + DAC7678Output() {} + + void register_channel(DAC7678Channel *channel); + + void set_internal_reference(const bool value) { this->internal_reference_ = value; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + friend DAC7678Channel; + + bool internal_reference_; + + void set_channel_value_(uint8_t channel, uint16_t value); + + uint8_t min_channel_{0xFF}; + uint8_t max_channel_{0x00}; + uint16_t dac_input_reg_[8] = { + 0, + }; +}; + +} // namespace dac7678 +} // namespace esphome diff --git a/esphome/components/dac7678/output.py b/esphome/components/dac7678/output.py new file mode 100644 index 0000000000..f41e5c2422 --- /dev/null +++ b/esphome/components/dac7678/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 DAC7678Output, dac7678_ns + +DEPENDENCIES = ["dac7678"] + +DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput) +CONF_DAC7678_ID = "dac7678_id" + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(DAC7678Channel), + cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_DAC7678_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(paren.register_channel(var)) + await output.register_output(var, config) + return var diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index b1d28b8b4c..302422d6c7 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -134,7 +134,6 @@ void DallasComponent::update() { return; } if (!sensor->check_scratch_pad()) { - ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str()); sensor->publish_state(NAN); this->status_set_warning(); return; @@ -241,13 +240,29 @@ bool DallasTemperatureSensor::setup_sensor() { return true; } bool DallasTemperatureSensor::check_scratch_pad() { + bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]); + bool config_validity = false; + + switch (this->get_address8()[0]) { + case DALLAS_MODEL_DS18B20: + config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F); + break; + default: + config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10); + } + #ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], crc8(this->scratch_pad_, 8)); #endif - return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]; + if (!chksum_validity) { + ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); + } else if (!config_validity) { + ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str()); + } + return chksum_validity && config_validity; } float DallasTemperatureSensor::get_temp_c() { int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3); diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index 45b8f98f0c..ce0cf5216a 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_ADDRESS CODEOWNERS = ["@s1lvi0"] DEPENDENCIES = ["uart"] @@ -15,7 +15,12 @@ DalyBmsComponent = daly_bms.class_( ) CONFIG_SCHEMA = ( - cv.Schema({cv.GenerateID(): cv.declare_id(DalyBmsComponent)}) + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DalyBmsComponent), + cv.Optional(CONF_ADDRESS, default=0x80): cv.positive_int, + } + ) .extend(uart.UART_DEVICE_SCHEMA) .extend(cv.polling_component_schema("30s")) ) @@ -25,3 +30,4 @@ 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_address(config[CONF_ADDRESS])) diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index f2b4c0e92b..36047e02d0 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -50,7 +50,7 @@ void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; request_message[0] = 0xA5; // Start Flag - request_message[1] = 0x80; // Communication Module Address + request_message[1] = addr_; // Communication Module Address request_message[2] = data_id; // Data ID request_message[3] = 0x08; // Data Length (Fixed) request_message[4] = 0x00; // Empty Data diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index 90faab77f7..44915368ee 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -69,11 +69,14 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void update() override; float get_setup_priority() const override; + void set_address(uint8_t address) { this->addr_ = address; } protected: void request_data_(uint8_t data_id); void decode_data_(std::vector data); + uint8_t addr_; + sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *battery_level_sensor_{nullptr}; diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 3cdfc8ab85..caa05c27b5 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -348,7 +348,7 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args): } ), ) -async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): +async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index ca866a43d2..97c08dae24 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -591,6 +591,18 @@ void Animation::prev_frame() { } } +void Animation::set_frame(int frame) { + unsigned abs_frame = abs(frame); + + if (abs_frame < this->animation_frame_count_) { + if (frame >= 0) { + this->current_frame_ = frame; + } else { + this->current_frame_ = this->animation_frame_count_ - abs_frame; + } + } +} + DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show_next() { this->next_->show(); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index dc6bbdf350..d2d3f2ed77 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -491,6 +491,12 @@ class Animation : public Image { void next_frame(); void prev_frame(); + /** Selects a specific frame within the animation. + * + * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. + */ + void set_frame(int frame); + protected: int current_frame_; int animation_frame_count_; diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index bf6d5445f1..3114dee359 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -132,7 +132,7 @@ class ColorUtil { int16_t plt_r = (int16_t) palette[i * 3 + 0]; int16_t plt_g = (int16_t) palette[i * 3 + 1]; int16_t plt_b = (int16_t) palette[i * 3 + 2]; - // Calculate euclidian distance (linear distance in rgb cube). + // Calculate euclidean distance (linear distance in rgb cube). x = (uint32_t) std::abs(tgt_r - plt_r); y = (uint32_t) std::abs(tgt_g - plt_g); z = (uint32_t) std::abs(tgt_b - plt_b); diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 624a0f35a1..284733cca6 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -85,4 +85,4 @@ async def to_code(config): cg.add_library("glmnet/Dsmr", "0.5") # Crypto - cg.add_library("rweather/Crypto", "0.2.0") + cg.add_library("rweather/Crypto", "0.4.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 7b339e5fe0..f382730912 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -171,7 +171,7 @@ void Dsmr::receive_telegram_() { this->telegram_[this->bytes_read_] = c; this->bytes_read_++; - // Check for a footer, i.e. exlamation mark, followed by a hex checksum. + // Check for a footer, i.e. exclamation mark, followed by a hex checksum. if (c == '!') { ESP_LOGV(TAG, "Footer of telegram found"); this->footer_found_ = true; diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 648cfb4585..8bf8999c21 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -4,9 +4,10 @@ #include "esphome/core/component.h" +#include #include #include -#include +#include class UDP; diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 9a89e85da2..a9c519856b 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -199,7 +199,7 @@ void ENS210Component::update() { }); } -// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment. +// Extracts measurement 'data' and 'status' from a 'val' obtained from measurement. void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) { *data = (val >> 0) & 0xffff; int valid = (val >> 16) & 0x1; diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 56fd4932b4..f14aeefea2 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -521,6 +521,33 @@ ESP32_BOARD_PINS = { }, "lolin32": {"LED": 5}, "lolin32_lite": {"LED": 22}, + "lolin_c3_mini": { + "TX": 21, + "RX": 20, + "SDA": 8, + "SCL": 10, + "SS": 5, + "MOSI": 4, + "MISO": 3, + "SCK": 2, + "A0": 0, + "A1": 1, + "A2": 2, + "A3": 3, + "A4": 4, + "A5": 5, + "D0": 1, + "D1": 10, + "D2": 8, + "D3": 7, + "D4": 6, + "D5": 2, + "D6": 3, + "D7": 4, + "D8": 5, + "LED": 7, + "BUTTON": 9, + }, "lolin_d32": {"LED": 5, "_VBAT": 35}, "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, "lopy": { @@ -1026,6 +1053,7 @@ BOARD_TO_VARIANT = { "labplus_mpython": VARIANT_ESP32, "lolin32_lite": VARIANT_ESP32, "lolin32": VARIANT_ESP32, + "lolin_c3_mini": VARIANT_ESP32C3, "lolin_d32_pro": VARIANT_ESP32, "lolin_d32": VARIANT_ESP32, "lopy4": VARIANT_ESP32, diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index dbafb73dba..66ba2ffa62 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -42,7 +42,7 @@ def esp32_validate_gpio_pin(value): "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", value, ) - if value in (20, 24, 28, 29, 30, 31): + if value in (24, 28, 29, 30, 31): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index aa03c5acc7..d070a20d82 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -36,6 +36,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { save.key = key; save.data.assign(data, data + len); s_pending_save.emplace_back(save); + ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len); return true; } bool load(uint8_t *data, size_t len) override { @@ -65,6 +66,8 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); return false; + } else { + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len); } return true; } @@ -73,7 +76,6 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { class ESP32Preferences : public ESPPreferences { public: uint32_t nvs_handle; - uint32_t current_offset = 0; void open() { nvs_flash_init(); @@ -97,12 +99,9 @@ class ESP32Preferences : public ESPPreferences { ESPPreferenceObject make_preference(size_t length, uint32_t type) override { auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->nvs_handle = nvs_handle; - current_offset += length; - uint32_t keyval = current_offset ^ type; - char keybuf[16]; - snprintf(keybuf, sizeof(keybuf), "%d", keyval); - pref->key = keybuf; // copied to std::string + uint32_t keyval = type; + pref->key = str_sprintf("%u", keyval); return ESPPreferenceObject(pref); } @@ -111,9 +110,11 @@ class ESP32Preferences : public ESPPreferences { if (s_pending_save.empty()) return true; - ESP_LOGD(TAG, "Saving preferences to flash..."); + ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size()); // goal try write all pending saves even if one fails - bool any_failed = false; + int cached = 0, written = 0, failed = 0; + esp_err_t last_err = ESP_OK; + std::string last_key{}; // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { @@ -121,17 +122,28 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); if (is_changed(nvs_handle, save)) { esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size()); if (err != 0) { ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), esp_err_to_name(err)); - any_failed = true; + failed++; + last_err = err; + last_key = save.key; continue; } + written++; } else { - ESP_LOGD(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); + ESP_LOGV(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); + cached++; } s_pending_save.erase(s_pending_save.begin() + i); } + ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached, + written, failed); + if (failed > 0) { + ESP_LOGD(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err), + last_key.c_str()); + } // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes esp_err_t err = nvs_commit(nvs_handle); @@ -140,7 +152,7 @@ class ESP32Preferences : public ESPPreferences { return false; } - return !any_failed; + return failed == 0; } bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { NVSData stored_data{}; @@ -150,7 +162,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); return true; } - stored_data.data.reserve(actual_len); + stored_data.data.resize(actual_len); err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 9722104e25..82945dc771 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -100,7 +100,12 @@ void ESP32BLETracker::loop() { found = true; if (client->state() == ClientState::DISCOVERED) { esp_ble_gap_stop_scanning(); - if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { +#ifdef USE_ARDUINO + constexpr TickType_t block_time = 10L / portTICK_PERIOD_MS; +#else + constexpr TickType_t block_time = 0L; // PR #3594 +#endif + if (xSemaphoreTake(this->scan_end_lock_, block_time)) { xSemaphoreGive(this->scan_end_lock_); } } diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 165c00c960..7e23c9dc6e 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -317,7 +317,7 @@ void ESP32Camera::update_camera_parameters() { s->set_gainceiling(s, (gainceiling_t) this->agc_gain_ceiling_); /* update white balance mode */ s->set_wb_mode(s, (int) this->wb_mode_); // 0 to 4 - /* update test patern */ + /* update test pattern */ s->set_colorbar(s, this->test_pattern_); } diff --git a/esphome/components/feedback/__init__.py b/esphome/components/feedback/__init__.py new file mode 100644 index 0000000000..9ae2df986d --- /dev/null +++ b/esphome/components/feedback/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ianchi"] diff --git a/esphome/components/feedback/cover.py b/esphome/components/feedback/cover.py new file mode 100644 index 0000000000..450eb967b1 --- /dev/null +++ b/esphome/components/feedback/cover.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import binary_sensor, cover +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_CLOSE_ACTION, + CONF_CLOSE_DURATION, + CONF_CLOSE_ENDSTOP, + CONF_ID, + CONF_OPEN_ACTION, + CONF_OPEN_DURATION, + CONF_OPEN_ENDSTOP, + CONF_STOP_ACTION, + CONF_MAX_DURATION, + CONF_UPDATE_INTERVAL, +) + +CONF_OPEN_SENSOR = "open_sensor" +CONF_CLOSE_SENSOR = "close_sensor" +CONF_OPEN_OBSTACLE_SENSOR = "open_obstacle_sensor" +CONF_CLOSE_OBSTACLE_SENSOR = "close_obstacle_sensor" +CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" +CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement" +CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time" +CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time" +CONF_OBSTACLE_ROLLBACK = "obstacle_rollback" + +endstop_ns = cg.esphome_ns.namespace("feedback") +FeedbackCover = endstop_ns.class_("FeedbackCover", cover.Cover, cg.Component) + + +def validate_infer_endstop(config): + if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] is True: + if config[CONF_HAS_BUILT_IN_ENDSTOP] is False: + raise cv.Invalid( + f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} can only be set if {CONF_HAS_BUILT_IN_ENDSTOP} is also set" + ) + + if CONF_OPEN_SENSOR not in config: + raise cv.Invalid( + f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if movement sensors are not supplied" + ) + + if CONF_OPEN_ENDSTOP in config or CONF_CLOSE_ENDSTOP in config: + raise cv.Invalid( + f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if endstop sensors are supplied" + ) + + return config + + +CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FeedbackCover), + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE): cv.boolean, + cv.Optional( + CONF_UPDATE_INTERVAL, "1000ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean, + cv.Optional( + CONF_DIRECTION_CHANGE_WAIT_TIME + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_ACCELERATION_WAIT_TIME, "0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, + }, +).extend(cv.COMPONENT_SCHEMA) + + +CONFIG_SCHEMA = cv.All( + CONFIG_FEEDBACK_COVER_BASE_SCHEMA, + cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR), + validate_infer_endstop, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + + # STOP + await automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) + + # OPEN + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) + if CONF_OPEN_ENDSTOP in config: + bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP]) + cg.add(var.set_open_endstop(bin)) + if CONF_OPEN_SENSOR in config: + bin = await cg.get_variable(config[CONF_OPEN_SENSOR]) + cg.add(var.set_open_sensor(bin)) + if CONF_OPEN_OBSTACLE_SENSOR in config: + bin = await cg.get_variable(config[CONF_OPEN_OBSTACLE_SENSOR]) + cg.add(var.set_open_obstacle_sensor(bin)) + + # CLOSE + await automation.build_automation( + var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] + ) + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) + if CONF_CLOSE_ENDSTOP in config: + bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP]) + cg.add(var.set_close_endstop(bin)) + if CONF_CLOSE_SENSOR in config: + bin = await cg.get_variable(config[CONF_CLOSE_SENSOR]) + cg.add(var.set_close_sensor(bin)) + if CONF_CLOSE_OBSTACLE_SENSOR in config: + bin = await cg.get_variable(config[CONF_CLOSE_OBSTACLE_SENSOR]) + cg.add(var.set_close_obstacle_sensor(bin)) + + # OTHER + if CONF_MAX_DURATION in config: + cg.add(var.set_max_duration(config[CONF_MAX_DURATION])) + + cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) + + if CONF_ASSUMED_STATE in config: + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) + else: + cg.add( + var.set_assumed_state( + not ( + (CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config) + or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] + ) + ) + ) + + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT])) + if CONF_DIRECTION_CHANGE_WAIT_TIME in config: + cg.add( + var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME]) + ) + cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME])) + cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK])) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp new file mode 100644 index 0000000000..213ce7ff8f --- /dev/null +++ b/esphome/components/feedback/feedback_cover.cpp @@ -0,0 +1,445 @@ +#include "feedback_cover.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace feedback { + +static const char *const TAG = "feedback.cover"; + +using namespace esphome::cover; + +void FeedbackCover::setup() { + auto restore = this->restore_state_(); + + if (restore.has_value()) { + restore->apply(this); + } else { + // if no other information, assume half open + this->position = 0.5f; + } + this->current_operation = COVER_OPERATION_IDLE; + +#ifdef USE_BINARY_SENSOR + // if available, get position from endstop sensors + if (this->open_endstop_ != nullptr && this->open_endstop_->state) { + this->position = COVER_OPEN; + } else if (this->close_endstop_ != nullptr && this->close_endstop_->state) { + this->position = COVER_CLOSED; + } + + // if available, get moving state from sensors + if (this->open_feedback_ != nullptr && this->open_feedback_->state) { + this->current_operation = COVER_OPERATION_OPENING; + } else if (this->close_feedback_ != nullptr && this->close_feedback_->state) { + this->current_operation = COVER_OPERATION_CLOSING; + } +#endif + + this->last_recompute_time_ = this->start_dir_time_ = millis(); +} + +CoverTraits FeedbackCover::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_position(true); + traits.set_supports_toggle(true); + traits.set_is_assumed_state(this->assumed_state_); + return traits; +} + +void FeedbackCover::dump_config() { + LOG_COVER("", "Endstop Cover", this); + ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); + LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_); + LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_); +#endif + ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); + LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_); + LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_); +#endif + if (this->has_built_in_endstop_) { + ESP_LOGCONFIG(TAG, " Has builtin endstop: YES"); + } + if (this->infer_endstop_) { + ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES"); + } + if (this->max_duration_ < UINT32_MAX) { + ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f); + } + if (this->direction_change_waittime_.has_value()) { + ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f); + } + if (this->acceleration_wait_time_) { + ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f); + } +#ifdef USE_BINARY_SENSOR + if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) { + ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100); + } +#endif +} + +#ifdef USE_BINARY_SENSOR + +void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) { + this->open_feedback_ = open_feedback; + + // setup callbacks to react to sensor changes + open_feedback->add_on_state_callback([this](bool state) { + ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); + this->recompute_position_(); + if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) { + this->endstop_reached_(true); + } + this->set_current_operation_(state ? COVER_OPERATION_OPENING : COVER_OPERATION_IDLE, false); + }); +} + +void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback) { + this->close_feedback_ = close_feedback; + + close_feedback->add_on_state_callback([this](bool state) { + ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); + this->recompute_position_(); + if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) { + this->endstop_reached_(false); + } + + this->set_current_operation_(state ? COVER_OPERATION_CLOSING : COVER_OPERATION_IDLE, false); + }); +} + +void FeedbackCover::set_open_endstop(binary_sensor::BinarySensor *open_endstop) { + this->open_endstop_ = open_endstop; + open_endstop->add_on_state_callback([this](bool state) { + if (state) { + this->endstop_reached_(true); + } + }); +} + +void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop) { + this->close_endstop_ = close_endstop; + close_endstop->add_on_state_callback([this](bool state) { + if (state) { + this->endstop_reached_(false); + } + }); +} +#endif + +void FeedbackCover::endstop_reached_(bool open_endstop) { + const uint32_t now = millis(); + + this->position = open_endstop ? COVER_OPEN : COVER_CLOSED; + + // only act if endstop activated while moving in the right direction, in case we are coming back + // from a position slightly past the endpoint + if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) { + float dur = (now - this->start_dir_time_) / 1e3f; + ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur); + + // if there is no external mechanism, stop the cover + if (!this->has_built_in_endstop_) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->set_current_operation_(COVER_OPERATION_IDLE, true); + } + } + + // always sync position and publish + this->publish_state(); + this->last_publish_time_ = now; +} + +void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool is_triggered) { + if (is_triggered) { + this->current_trigger_operation_ = operation; + } + + // if it is setting the actual operation (not triggered one) or + // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately + // thus, triggered operation always sets current operation. + // otherwise, current operation comes from sensor, and may differ from requested operation + // this might be from delays or complex actions, or because the movement was not trigger by the component + // but initiated externally + +#ifdef USE_BINARY_SENSOR + if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr)) +#endif + { + auto now = millis(); + this->current_operation = operation; + this->start_dir_time_ = this->last_recompute_time_ = now; + this->publish_state(); + this->last_publish_time_ = now; + } +} + +#ifdef USE_BINARY_SENSOR +void FeedbackCover::set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle) { + this->close_obstacle_ = close_obstacle; + + close_obstacle->add_on_state_callback([this](bool state) { + if (state && (this->current_operation == COVER_OPERATION_CLOSING || + this->current_trigger_operation_ == COVER_OPERATION_CLOSING)) { + ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + + if (this->obstacle_rollback_) { + this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN); + this->start_direction_(COVER_OPERATION_OPENING); + } + } + }); +} + +void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) { + this->open_obstacle_ = open_obstacle; + + open_obstacle->add_on_state_callback([this](bool state) { + if (state && (this->current_operation == COVER_OPERATION_OPENING || + this->current_trigger_operation_ == COVER_OPERATION_OPENING)) { + ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + + if (this->obstacle_rollback_) { + this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN); + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + }); +} +#endif + +void FeedbackCover::loop() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + const uint32_t now = millis(); + + // Recompute position every loop cycle + this->recompute_position_(); + + // if we initiated the move, check if we reached position or max time + // (stoping from endstop sensor is handled in callback) + if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) { + if (this->is_at_target_()) { + if (this->has_built_in_endstop_ && + (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) { + // Don't trigger stop, let the cover stop by itself. + this->set_current_operation_(COVER_OPERATION_IDLE, true); + } else { + this->start_direction_(COVER_OPERATION_IDLE); + } + } else if (now - this->start_dir_time_ > this->max_duration_) { + ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + } + } + + // update current position at requested interval, regardless of who started the movement + // so that we also update UI if there was an external movement + // don´t save intermediate positions + if (now - this->last_publish_time_ > this->update_interval_) { + this->publish_state(false); + this->last_publish_time_ = now; + } +} + +void FeedbackCover::control(const CoverCall &call) { + // stop action logic + if (call.get_stop()) { + this->start_direction_(COVER_OPERATION_IDLE); + } else if (call.get_toggle().has_value()) { + // toggle action logic: OPEN - STOP - CLOSE + if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { + this->target_position_ = COVER_OPEN; + this->start_direction_(COVER_OPERATION_OPENING); + } else { + this->target_position_ = COVER_CLOSED; + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + } else if (call.get_position().has_value()) { + // go to position action + auto pos = *call.get_position(); + if (pos == this->position) { + // already at target, + + // for covers with built in end stop, if we don´t have sensors we should send the command again + // to make sure the assumed state is not wrong + if (this->has_built_in_endstop_ && ((pos == COVER_OPEN +#ifdef USE_BINARY_SENSOR + && this->open_endstop_ == nullptr +#endif + && !this->infer_endstop_) || + (pos == COVER_CLOSED +#ifdef USE_BINARY_SENSOR + && this->close_endstop_ == nullptr +#endif + && !this->infer_endstop_))) { + this->target_position_ = pos; + this->start_direction_(pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } else if (this->current_operation != COVER_OPERATION_IDLE || + this->current_trigger_operation_ != COVER_OPERATION_IDLE) { + // if we are moving, stop + this->start_direction_(COVER_OPERATION_IDLE); + } + } else { + this->target_position_ = pos; + this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } + } +} + +void FeedbackCover::stop_prev_trigger_() { + if (this->direction_change_waittime_.has_value()) { + this->cancel_timeout("direction_change"); + } + if (this->prev_command_trigger_ != nullptr) { + this->prev_command_trigger_->stop_action(); + this->prev_command_trigger_ = nullptr; + } +} + +bool FeedbackCover::is_at_target_() const { + // if initiated externally, current operation might be different from + // operation that was triggered, thus evaluate position against what was asked + + switch (this->current_trigger_operation_) { + case COVER_OPERATION_OPENING: + return this->position >= this->target_position_; + case COVER_OPERATION_CLOSING: + return this->position <= this->target_position_; + case COVER_OPERATION_IDLE: + return this->current_operation == COVER_OPERATION_IDLE; + default: + return true; + } +} +void FeedbackCover::start_direction_(CoverOperation dir) { + Trigger<> *trig; + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *obstacle{nullptr}; +#endif + + switch (dir) { + case COVER_OPERATION_IDLE: + trig = this->stop_trigger_; + break; + case COVER_OPERATION_OPENING: + this->last_operation_ = dir; + trig = this->open_trigger_; +#ifdef USE_BINARY_SENSOR + obstacle = this->open_obstacle_; +#endif + break; + case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; + trig = this->close_trigger_; +#ifdef USE_BINARY_SENSOR + obstacle = this->close_obstacle_; +#endif + break; + default: + return; + } + + this->stop_prev_trigger_(); + +#ifdef USE_BINARY_SENSOR + // check if there is an obstacle to start the new operation -> abort without any change + // the case when an obstacle appears while moving is handled in the callback + if (obstacle != nullptr && obstacle->state) { + ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "Open" : "Close"); + return; + } +#endif + + // if we are moving and need to move in the opposite direction + // check if we have a wait time + if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE && + this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) { + ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + + this->set_timeout("direction_change", *this->direction_change_waittime_, + [this, dir]() { this->start_direction_(dir); }); + + } else { + this->set_current_operation_(dir, true); + this->prev_command_trigger_ = trig; + ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "OPEN" + : dir == COVER_OPERATION_CLOSING ? "CLOSE" + : "STOP"); + trig->trigger(); + } +} + +void FeedbackCover::recompute_position_() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + const uint32_t now = millis(); + float dir; + float action_dur; + float min_pos; + float max_pos; + + // endstop sensors update position from their callbacks, and sets the fully open/close value + // If we have endstop, estimation never reaches the fully open/closed state. + // but if movement continues past corresponding endstop (inertia), keep the fully open/close state + + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + dir = 1.0f; + action_dur = this->open_duration_; + min_pos = COVER_CLOSED; + max_pos = ( +#ifdef USE_BINARY_SENSOR + this->open_endstop_ != nullptr || +#endif + this->infer_endstop_) && + this->position < COVER_OPEN + ? 0.99f + : COVER_OPEN; + break; + case COVER_OPERATION_CLOSING: + dir = -1.0f; + action_dur = this->close_duration_; + min_pos = ( +#ifdef USE_BINARY_SENSOR + this->close_endstop_ != nullptr || +#endif + this->infer_endstop_) && + this->position > COVER_CLOSED + ? 0.01f + : COVER_CLOSED; + max_pos = COVER_OPEN; + break; + default: + return; + } + + // check if we have an acceleration_wait_time, and remove from position computation + if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) { + this->position += + dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) / + (action_dur - this->acceleration_wait_time_); + this->position = clamp(this->position, min_pos, max_pos); + } + this->last_recompute_time_ = now; +} + +} // namespace feedback +} // namespace esphome diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h new file mode 100644 index 0000000000..7e107aebcd --- /dev/null +++ b/esphome/components/feedback/feedback_cover.h @@ -0,0 +1,90 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace feedback { + +class FeedbackCover : public cover::Cover, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + Trigger<> *get_open_trigger() const { return this->open_trigger_; } + Trigger<> *get_close_trigger() const { return this->close_trigger_; } + Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + +#ifdef USE_BINARY_SENSOR + void set_open_endstop(binary_sensor::BinarySensor *open_endstop); + void set_open_sensor(binary_sensor::BinarySensor *open_feedback); + void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle); + void set_close_endstop(binary_sensor::BinarySensor *close_endstop); + void set_close_sensor(binary_sensor::BinarySensor *close_feedback); + void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle); +#endif + void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } + void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } + void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } + void set_assumed_state(bool value) { this->assumed_state_ = value; } + void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; } + void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; } + void set_update_interval(uint32_t interval) { this->update_interval_ = interval; } + void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; } + void set_direction_change_waittime(uint32_t waittime) { this->direction_change_waittime_ = waittime; } + void set_acceleration_wait_time(uint32_t waittime) { this->acceleration_wait_time_ = waittime; } + + cover::CoverTraits get_traits() override; + + protected: + void control(const cover::CoverCall &call) override; + void stop_prev_trigger_(); + bool is_at_target_() const; + void start_direction_(cover::CoverOperation dir); + void update_operation_(cover::CoverOperation dir); + void endstop_reached_(bool open_endstop); + void recompute_position_(); + void set_current_operation_(cover::CoverOperation operation, bool is_triggered); + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *open_endstop_{nullptr}; + binary_sensor::BinarySensor *close_endstop_{nullptr}; + binary_sensor::BinarySensor *open_feedback_{nullptr}; + binary_sensor::BinarySensor *close_feedback_{nullptr}; + binary_sensor::BinarySensor *open_obstacle_{nullptr}; + binary_sensor::BinarySensor *close_obstacle_{nullptr}; + +#endif + Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> *stop_trigger_{new Trigger<>()}; + + uint32_t open_duration_{0}; + uint32_t close_duration_{0}; + uint32_t max_duration_{UINT32_MAX}; + optional direction_change_waittime_{}; + uint32_t acceleration_wait_time_{0}; + bool has_built_in_endstop_{false}; + bool assumed_state_{false}; + bool infer_endstop_{false}; + float obstacle_rollback_{0}; + + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; + cover::CoverOperation current_trigger_operation_{cover::COVER_OPERATION_IDLE}; + Trigger<> *prev_command_trigger_{nullptr}; + uint32_t last_recompute_time_{0}; + uint32_t start_dir_time_{0}; + uint32_t last_publish_time_{0}; + float target_position_{0}; + uint32_t update_interval_{1000}; +}; + +} // namespace feedback +} // namespace esphome diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 3286e43575..101adeb311 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -44,7 +44,14 @@ template class RestoringGlobalsComponent : public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override { + void loop() override { store_value_(); } + + void on_shutdown() override { store_value_(); } + + void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } + + protected: + void store_value_() { int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T)); if (diff != 0) { this->rtc_.save(&this->value_); @@ -52,9 +59,6 @@ template class RestoringGlobalsComponent : public Component { } } - void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } - - protected: T value_{}; T prev_value_{}; uint32_t name_hash_{}; diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 12acfee869..046f59ca1a 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -118,7 +118,7 @@ def _relocate_fields_to_subfolder(config, subfolder, subschema): fields = [k.schema for k in subschema.schema.keys()] fields.remove(CONF_ID) if subfolder in config: - # Ensure no ambigious fields in base of config + # Ensure no ambiguous fields in base of config for f in fields: if f in config: raise cv.Invalid( diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index cfdf818112..2e92468659 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -224,7 +224,7 @@ void ArduinoI2CBus::recover_() { digitalWrite(sda_pin_, LOW); // NOLINT // By now, any stuck device ought to have sent all remaining bits of its - // transation, meaning that it should have freed up the SDA line, resulting + // transaction, meaning that it should have freed up the SDA line, resulting // in SDA being pulled up. if (digitalRead(sda_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 160b1b96d8..1e2a7304f2 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -285,7 +285,7 @@ void IDFI2CBus::recover_() { } // By now, any stuck device ought to have sent all remaining bits of its - // transation, meaning that it should have freed up the SDA line, resulting + // transaction, meaning that it should have freed up the SDA line, resulting // in SDA being pulled up. if (gpio_get_level(sda_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 642116152c..65fa42dd0d 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,14 +10,13 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); float preference_value = 0; - this->rtc_.load(&preference_value); + this->pref_.load(&preference_value); this->result_ = preference_value; } this->last_update_ = millis(); - this->last_save_ = this->last_update_; this->publish_and_save_(this->result_); this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); }); diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 1d46973086..e84d7a8ed1 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -28,7 +28,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } - void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } @@ -56,22 +55,18 @@ class IntegrationSensor : public sensor::Sensor, public Component { void publish_and_save_(double result) { this->result_ = result; this->publish_state(result); - float result_f = result; - const uint32_t now = millis(); - if (now - this->last_save_ < this->min_save_interval_) - return; - this->last_save_ = now; - this->rtc_.save(&result_f); + if (this->restore_) { + float result_f = result; + this->pref_.save(&result_f); + } } sensor::Sensor *sensor_; IntegrationSensorTime time_; IntegrationMethod method_; bool restore_; - ESPPreferenceObject rtc_; + ESPPreferenceObject pref_; - uint32_t last_save_{0}; - uint32_t min_save_interval_{0}; uint32_t last_update_; double result_{0.0f}; float last_value_{0.0f}; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index c35d42f385..3d7cf03882 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -35,7 +35,6 @@ INTEGRATION_METHODS = { CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" -CONF_MIN_SAVE_INTERVAL = "min_save_interval" def inherit_unit_of_measurement(uom, config): @@ -58,9 +57,9 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( INTEGRATION_METHODS, lower=True ), cv.Optional(CONF_RESTORE, default=False): cv.boolean, - cv.Optional( - CONF_MIN_SAVE_INTERVAL, default="0s" - ): cv.positive_time_period_milliseconds, + cv.Optional("min_save_interval"): cv.invalid( + "min_save_interval was removed in 2022.8.0. Please use the `preferences` -> `flash_write_interval` to adjust." + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -97,7 +96,6 @@ async def to_code(config): cg.add(var.set_time(config[CONF_TIME_UNIT])) cg.add(var.set_method(config[CONF_INTEGRATION_METHOD])) cg.add(var.set_restore(config[CONF_RESTORE])) - cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) @automation.register_action( diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index a987e0fc96..cef7cd7f3a 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -1,4 +1,4 @@ -from esphome.jsonschema import jschema_extractor +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -479,11 +479,11 @@ async def addressable_flicker_effect_to_code(config, effect_id): def validate_effects(allowed_effects): - @jschema_extractor("effects") + @schema_extractor("effects") def validator(value): - # pylint: disable=comparison-with-callable - if value == jschema_extractor: + if value == SCHEMA_EXTRACT: return (allowed_effects, EFFECTS_REGISTRY) + value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value) errors = [] names = set() diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index ffbe378ee3..10a3c2f335 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -203,7 +203,7 @@ class LightColorValues { *color_temperature = (this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); *white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma); - } else { // Probably wont get here but put this here anyway. + } else { // Probably won't get here but put this here anyway. *white_brightness = 0; } } diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index c126859076..6f8cc11f25 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -121,7 +121,7 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO call.set_cold_white(float(color["c"]) / 255.0f); } if (color.containsKey("w")) { - // the HA scheme is ambigious here, the same key is used for white channel in RGBW and warm + // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm // white channel in RGBWW. if (color.containsKey("c")) { call.set_warm_white(float(color["w"]) / 255.0f); diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index f2c2706416..7bcd5c84fc 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -19,7 +19,6 @@ CODEOWNERS = ["@jesserockz"] mcp23xxx_base_ns = cg.esphome_ns.namespace("mcp23xxx_base") MCP23XXXBase = mcp23xxx_base_ns.class_("MCP23XXXBase", cg.Component) MCP23XXXGPIOPin = mcp23xxx_base_ns.class_("MCP23XXXGPIOPin", cg.GPIOPin) -MCP23XXXGPIOMode = mcp23xxx_base_ns.enum("MCP23XXXGPIOMode") MCP23XXXInterruptMode = mcp23xxx_base_ns.enum("MCP23XXXInterruptMode") MCP23XXX_INTERRUPT_MODES = { @@ -29,12 +28,6 @@ MCP23XXX_INTERRUPT_MODES = { "FALLING": MCP23XXXInterruptMode.MCP23XXX_FALLING, } -MCP23XXX_GPIO_MODES = { - "INPUT": MCP23XXXGPIOMode.MCP23XXX_INPUT, - "INPUT_PULLUP": MCP23XXXGPIOMode.MCP23XXX_INPUT_PULLUP, - "OUTPUT": MCP23XXXGPIOMode.MCP23XXX_OUTPUT, -} - MCP23XXX_CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, @@ -95,20 +88,3 @@ async def mcp23xxx_pin_to_code(config): cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) cg.add(var.set_interrupt_mode(config[CONF_INTERRUPT])) return var - - -# BEGIN Removed pin schemas below to show error in configuration -# TODO remove in 2022.5.0 - -for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: - invalid_schema = cv.invalid( - f"'{id}:' has been removed from the pin schema in 1.17.0, please use 'mcp23xxx:'" - ) - - # pylint: disable=cell-var-from-loop - @pins.PIN_SCHEMA_REGISTRY.register(id, invalid_schema) - def pin_to_code(config): - pass - - -# END Removed pin schemas diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 168eaf3ae1..31858c0d3c 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -68,6 +68,16 @@ void MDNSComponent::compile_records_() { } #endif +#ifdef USE_WEBSERVER + { + MDNSService service{}; + service.service_type = "_http"; + service.proto = "_tcp"; + service.port = USE_WEBSERVER_PORT; + this->services_.push_back(service); + } +#endif + 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/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 19b5e8019e..845fa92e95 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -76,7 +76,12 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // installed, but wait, there is the CRC, and if we get a hit there is a good // chance that this is a complete message ... admittedly there is a small chance is // isn't but that is quite small given the purpose of the CRC in the first place - data_len = at; + + // Fewer than 2 bytes can't calc CRC + if (at < 2) + return true; + + data_len = at - 2; data_offset = 1; uint16_t computed_crc = crc16(raw, data_offset + data_len); @@ -95,7 +100,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } // Error ( msb indicates error ) - // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc + // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc if ((function_code & 0x80) == 0x80) { data_offset = 2; data_len = 1; diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index b7fde157d8..bd2fec3ece 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -70,7 +70,7 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { ESP_LOGE(TAG, - "Modbus error - last command: function code=0x%X register adddress = 0x%X " + "Modbus error - last command: function code=0x%X register address = 0x%X " "registers count=%d " "payload size=%zu", function_code, current_command->register_address, current_command->register_count, @@ -105,7 +105,7 @@ void ModbusController::on_register_data(ModbusRegisterType register_type, uint16 } void ModbusController::queue_command(const ModbusCommandItem &command) { - // check if this commmand is already qeued. + // check if this command is already qeued. // not very effective but the queue is never really large for (auto &item : command_queue_) { if (item->is_equal(command)) { @@ -298,7 +298,7 @@ void ModbusController::loop() { incoming_queue_.pop(); } else { - // all messages processed send pending commmands + // all messages processed send pending commands send_next_command_(); } } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index f67242a68e..512fe0b25d 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -185,8 +185,8 @@ inline bool coil_from_vector(int coil, const std::vector &data) { /** Extract bits from value and shift right according to the bitmask * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. - * the result is then shifted right by the postion if the first right set bit in the mask - * Usefull for modbus data where more than one value is packed in a 16 bit register + * the result is then shifted right by the position if the first right set bit in the mask + * Useful for modbus data where more than one value is packed in a 16 bit register * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as * D15 - D8 = hour, D7 - D0 = minute * To get the hours use mask 0xFF00 and 0x00FF for the minute @@ -449,7 +449,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void dump_sensors_(); /// Collection of all sensors for this component SensorSet sensorset_; - /// Continous range of modbus registers + /// Continuous range of modbus registers std::vector register_ranges_; /// Hold the pending requests to be sent std::list> command_queue_; diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 37a39ff334..52f63e791b 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -68,7 +68,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - # 24 bits are the maximum value for fp32 before precison is lost + # 24 bits are the maximum value for fp32 before precision is lost # 0x00FFFFFF = 16777215 cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_, cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_, diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 5f4219acb1..210d7b2e2b 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -26,5 +26,12 @@ class WakeTrigger : public Trigger<> { } }; +class PageTrigger : public Trigger { + public: + explicit PageTrigger(Nextion *nextion) { + nextion->add_new_page_callback([this](const uint8_t page_id) { this->trigger(page_id); }); + } +}; + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 75694ee4b2..06216e9ce0 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -18,6 +18,7 @@ CONF_TFT_URL = "tft_url" CONF_ON_SLEEP = "on_sleep" CONF_ON_WAKE = "on_wake" CONF_ON_SETUP = "on_setup" +CONF_ON_PAGE = "on_page" CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" CONF_WAKE_UP_PAGE = "wake_up_page" CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index d95810bfbe..c6372fbaf0 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -14,6 +14,7 @@ from .base_component import ( CONF_ON_SLEEP, CONF_ON_WAKE, CONF_ON_SETUP, + CONF_ON_PAGE, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, @@ -28,6 +29,7 @@ AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) +PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( @@ -50,6 +52,11 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), } ), + cv.Optional(CONF_ON_PAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), + } + ), cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, @@ -102,3 +109,7 @@ async def to_code(config): for conf in config.get(CONF_ON_WAKE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_PAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint8, "x")], conf) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 46c063e5ee..5dfff4327c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -152,6 +152,10 @@ void Nextion::add_setup_state_callback(std::function &&callback) { this->setup_callback_.add(std::move(callback)); } +void Nextion::add_new_page_callback(std::function &&callback) { + this->page_callback_.add(std::move(callback)); +} + void Nextion::update_all_components() { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; @@ -390,7 +394,6 @@ void Nextion::process_nextion_commands_() { case 0x1A: // variable name invalid ESP_LOGW(TAG, "Nextion reported variable name invalid!"); this->remove_from_q_(); - break; case 0x1B: // variable operation invalid ESP_LOGW(TAG, "Nextion reported variable operation invalid!"); @@ -417,7 +420,6 @@ void Nextion::process_nextion_commands_() { case 0x23: // too long variable name ESP_LOGW(TAG, "Nextion reported too long variable name!"); this->remove_from_q_(); - break; case 0x24: // Serial Buffer overflow occurs ESP_LOGW(TAG, "Nextion reported Serial Buffer overflow!"); @@ -425,9 +427,9 @@ void Nextion::process_nextion_commands_() { case 0x65: { // touch event return data if (to_process_length != 3) { ESP_LOGW(TAG, "Touch event data is expecting 3, received %zu", to_process_length); - break; } + uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press @@ -438,6 +440,18 @@ void Nextion::process_nextion_commands_() { } break; } + case 0x66: { // Nextion initiated new page event return data. + // Also is used for sendme command which we never explicitly initiate + if (to_process_length != 1) { + ESP_LOGW(TAG, "New page event data is expecting 1, received %zu", to_process_length); + break; + } + + uint8_t page_id = to_process[0]; + ESP_LOGD(TAG, "Got new page=%u", page_id); + this->page_callback_.call(page_id); + break; + } case 0x67: { // Touch Coordinate (awake) break; } @@ -455,9 +469,6 @@ void Nextion::process_nextion_commands_() { ESP_LOGD(TAG, "Got touch at x=%u y=%u type=%s", x, y, touch_event ? "PRESS" : "RELEASE"); break; } - case 0x66: { - break; - } // sendme page id // 0x70 0x61 0x62 0x31 0x32 0x33 0xFF 0xFF 0xFF // Returned when using get command for a string. diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 285b3ac9a3..ad696d0e83 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -689,6 +689,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void add_setup_state_callback(std::function &&callback); + /** Add a callback to be notified when the nextion changes pages. + * + * @param callback The void(std::string) callback. + */ + void add_new_page_callback(std::function &&callback); + void update_all_components(); /** @@ -813,6 +819,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager setup_callback_{}; CallbackManager sleep_callback_{}; CallbackManager wake_callback_{}; + CallbackManager page_callback_{}; optional writer_; float brightness_{1.0}; diff --git a/esphome/components/nfc/ndef_record.cpp b/esphome/components/nfc/ndef_record.cpp index 8a3a7d375d..8eb0c3b901 100644 --- a/esphome/components/nfc/ndef_record.cpp +++ b/esphome/components/nfc/ndef_record.cpp @@ -12,7 +12,7 @@ NdefRecord::NdefRecord(std::vector payload_data) { std::vector NdefRecord::encode(bool first, bool last) { std::vector data; - // Get encoded payload, this is overriden by more specific record classes + // Get encoded payload, this is overridden by more specific record classes std::vector payload_data = get_encoded_payload(); size_t payload_length = payload_data.size(); diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index c75d272660..cadc6f54f6 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -6,8 +6,17 @@ namespace number { static const char *const TAG = "number.automation"; +union convert { + float from; + uint32_t to; +}; + void ValueRangeTrigger::setup() { - this->rtc_ = global_preferences->make_preference(this->parent_->get_object_id_hash()); + float local_min = this->min_.value(0.0); + float local_max = this->max_.value(0.0); + convert hash = {.from = (local_max - local_min)}; + uint32_t myhash = hash.to ^ this->parent_->get_object_id_hash(); + this->rtc_ = global_preferences->make_preference(myhash); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index c1935509f0..fab4705be7 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -768,7 +768,7 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { uint8_t Pipsolar::check_incoming_crc_() { uint16_t crc16; - crc16 = calc_crc_(read_buffer_, read_pos_ - 3); + crc16 = cal_crc_half_(read_buffer_, read_pos_ - 3); ESP_LOGD(TAG, "checking crc on incoming message"); if (((uint8_t)((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && ((uint8_t)((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { @@ -797,7 +797,7 @@ uint8_t Pipsolar::send_next_command_() { this->command_start_millis_ = millis(); this->empty_uart_buffer_(); this->read_pos_ = 0; - crc16 = calc_crc_(byte_command, length); + crc16 = cal_crc_half_(byte_command, length); this->write_str(command); // checksum this->write(((uint8_t)((crc16) >> 8))); // highbyte @@ -824,8 +824,8 @@ void Pipsolar::send_next_poll_() { this->command_start_millis_ = millis(); this->empty_uart_buffer_(); this->read_pos_ = 0; - crc16 = calc_crc_(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); + crc16 = cal_crc_half_(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); this->write_array(this->used_polling_commands_[this->last_polling_command_].command, this->used_polling_commands_[this->last_polling_command_].length); // checksum @@ -892,29 +892,41 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll } } -uint16_t Pipsolar::calc_crc_(uint8_t *msg, int n) { - // Initial value. xmodem uses 0xFFFF but this example - // requires an initial value of zero. - uint16_t x = 0; - while (n--) { - x = crc_xmodem_update_(x, (uint16_t) *msg++); - } - return (x); -} +uint16_t Pipsolar::cal_crc_half_(uint8_t *msg, uint8_t len) { + uint16_t crc; -// See bottom of this page: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html -// Polynomial: x^16 + x^12 + x^5 + 1 (0x1021) -uint16_t Pipsolar::crc_xmodem_update_(uint16_t crc, uint8_t data) { - int i; - crc = crc ^ ((uint16_t) data << 8); - for (i = 0; i < 8; i++) { - if (crc & 0x8000) { - crc = (crc << 1) ^ 0x1021; //(polynomial = 0x1021) - } else { - crc <<= 1; - } + uint8_t da; + uint8_t *ptr; + uint8_t b_crc_hign; + uint8_t b_crc_low; + + uint16_t crc_ta[16] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef}; + + ptr = msg; + crc = 0; + + while (len-- != 0) { + da = ((uint8_t)(crc >> 8)) >> 4; + crc <<= 4; + crc ^= crc_ta[da ^ (*ptr >> 4)]; + da = ((uint8_t)(crc >> 8)) >> 4; + crc <<= 4; + crc ^= crc_ta[da ^ (*ptr & 0x0f)]; + ptr++; } - return crc; + + b_crc_low = crc; + b_crc_hign = (uint8_t)(crc >> 8); + + if (b_crc_low == 0x28 || b_crc_low == 0x0d || b_crc_low == 0x0a) + b_crc_low++; + if (b_crc_hign == 0x28 || b_crc_hign == 0x0d || b_crc_hign == 0x0a) + b_crc_hign++; + + crc = ((uint16_t) b_crc_hign) << 8; + crc += b_crc_low; + return (crc); } } // namespace pipsolar diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index fe2a80d1d5..4f6edb4810 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -193,8 +193,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { void empty_uart_buffer_(); uint8_t check_incoming_crc_(); uint8_t check_incoming_length_(uint8_t length); - uint16_t calc_crc_(uint8_t *msg, int n); - uint16_t crc_xmodem_update_(uint16_t crc, uint8_t data); + uint16_t cal_crc_half_(uint8_t *msg, uint8_t len); uint8_t send_next_command_(); void send_next_poll_(); void queue_command_(const char *command, uint8_t length); diff --git a/esphome/components/pulse_counter/automation.h b/esphome/components/pulse_counter/automation.h new file mode 100644 index 0000000000..d749540a95 --- /dev/null +++ b/esphome/components/pulse_counter/automation.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/pulse_counter/pulse_counter_sensor.h" + +namespace esphome { + +namespace pulse_counter { + +template class SetTotalPulsesAction : public Action { + public: + SetTotalPulsesAction(PulseCounterSensor *pulse_counter) : pulse_counter_(pulse_counter) {} + + TEMPLATABLE_VALUE(uint32_t, total_pulses) + + void play(Ts... x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } + + protected: + PulseCounterSensor *pulse_counter_; +}; + +} // namespace pulse_counter +} // namespace esphome diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 5232ebc427..002f6dcac9 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -144,6 +144,11 @@ void PulseCounterSensor::setup() { } } +void PulseCounterSensor::set_total_pulses(uint32_t pulses) { + this->current_total_ = pulses; + this->total_sensor_->publish_state(pulses); +} + void PulseCounterSensor::dump_config() { LOG_SENSOR("", "Pulse Counter", this); LOG_PIN(" Pin: ", this->pin_); diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 86c387d52a..f81d20a646 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -55,6 +55,8 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { void set_filter_us(uint32_t filter) { storage_.filter_us = filter; } void set_total_sensor(sensor::Sensor *total_sensor) { total_sensor_ = total_sensor; } + void set_total_pulses(uint32_t pulses); + /// Unit of measurement is "pulses/min". void setup() override; void update() override; diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 6dcb974a1f..88f53bdf77 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -1,15 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import automation, pins from esphome.components import sensor from esphome.const import ( CONF_COUNT_MODE, CONF_FALLING_EDGE, + CONF_ID, CONF_INTERNAL_FILTER, CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, CONF_TOTAL, + CONF_VALUE, ICON_PULSE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -32,6 +34,10 @@ PulseCounterSensor = pulse_counter_ns.class_( "PulseCounterSensor", sensor.Sensor, cg.PollingComponent ) +SetTotalPulsesAction = pulse_counter_ns.class_( + "SetTotalPulsesAction", automation.Action +) + def validate_internal_filter(value): value = cv.positive_time_period_microseconds(value) @@ -116,3 +122,21 @@ async def to_code(config): if CONF_TOTAL in config: sens = await sensor.new_sensor(config[CONF_TOTAL]) cg.add(var.set_total_sensor(sens)) + + +@automation.register_action( + "pulse_counter.set_total_pulses", + SetTotalPulsesAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(PulseCounterSensor), + cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t), + } + ), +) +async def set_total_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALUE], args, int) + cg.add(var.set_total_pulses(template_)) + return var diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index a41ad1bfcb..6975109952 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -43,6 +43,8 @@ bool PVVXMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &devic this->battery_level_->publish_state(*res->battery_level); if (res->battery_voltage.has_value() && this->battery_voltage_ != nullptr) this->battery_voltage_->publish_state(*res->battery_voltage); + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(device.get_rssi()); success = true; } diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index ad8baed35f..9b8e2d9cfe 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -28,6 +28,7 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + void set_signal_strength(sensor::Sensor *signal_strength) { signal_strength_ = signal_strength; } protected: uint64_t address_; @@ -35,6 +36,7 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *humidity_{nullptr}; sensor::Sensor *battery_level_{nullptr}; sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py index 12090bddba..aa4fc89727 100644 --- a/esphome/components/pvvx_mithermometer/sensor.py +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -6,15 +6,18 @@ from esphome.const import ( CONF_BATTERY_VOLTAGE, CONF_MAC_ADDRESS, CONF_HUMIDITY, + CONF_SIGNAL_STRENGTH, CONF_TEMPERATURE, CONF_ID, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, UNIT_PERCENT, UNIT_VOLT, ) @@ -59,6 +62,13 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) @@ -85,3 +95,6 @@ async def to_code(config): if CONF_BATTERY_VOLTAGE in config: sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) cg.add(var.set_battery_voltage(sens)) + if CONF_SIGNAL_STRENGTH in config: + sens = await sensor.new_sensor(config[CONF_SIGNAL_STRENGTH]) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 5ccfc500cf..5730cba1eb 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -32,7 +32,7 @@ from esphome.const import ( CONF_LEVEL, ) from esphome.core import coroutine -from esphome.jsonschema import jschema_extractor +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ["binary_sensor"] @@ -195,14 +195,14 @@ def validate_dumpers(value): def validate_triggers(base_schema): assert isinstance(base_schema, cv.Schema) - @jschema_extractor("triggers") + @schema_extractor("triggers") def validator(config): added_keys = {} for key, (_, valid) in TRIGGER_REGISTRY.items(): added_keys[cv.Optional(key)] = valid new_schema = base_schema.extend(added_keys) - # pylint: disable=comparison-with-callable - if config == jschema_extractor: + + if config == SCHEMA_EXTRACT: return new_schema return new_schema(config) diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index dbdc835fe3..a0066ea5d4 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -106,7 +106,7 @@ optional NexaProtocol::decode(RemoteReceiveData src) { SHHHH HHHH HHHH HHHH HHHH HHHH HHGO EE BB DDDD 0 P S = Sync bit. - H = The first 26 bits are transmitter unique codes, and it is this code that the reciever "learns" to recognize. + H = The first 26 bits are transmitter unique codes, and it is this code that the receiver "learns" to recognize. G = Group code, set to one for the whole group. O = On/Off bit. Set to 1 for on, 0 for off. E = Unit to be turned on or off. The code is inverted, i.e. '11' equals 1, '00' equals 4. diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index b353f0254e..5b6284a86f 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -102,7 +102,7 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const { if (!src.peek_space(this->sync_low_, 1)) return false; } else { - // We cant peek a space at the beginning because signals starts with a low to high transition. + // We can't peek a space at the beginning because signals starts with a low to high transition. // this long space at the beginning is the separation between the transmissions itself, so it is actually // added at the end kind of artificially (by the value given to "idle:" option by the user in the yaml) if (!src.peek_mark(this->sync_low_)) diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index cbda996a4c..681324fa18 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -205,7 +205,7 @@ void SCD4XComponent::update() { bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) { /* Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power - periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2 + periodic measurement or single shot) for > 3 minutes in an environment with homogeneous and constant CO2 concentration before performing a forced recalibration. */ if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { @@ -217,7 +217,7 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); // frc takes 400 ms // because this method will be used very rarly - // the simple aproach with delay is ok + // the simple approach with delay is ok delay(400); // NOLINT' if (!this->start_measurement_()) { return false; diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index a2232a7d1b..d8ab817a73 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -33,7 +33,7 @@ bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { } /*** * write command with parameters and insert crc - * use stack array for less than 4 paramaters. Most sensirion i2c commands have less parameters + * use stack array for less than 4 parameters. Most sensirion i2c commands have less parameters */ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len) { @@ -63,7 +63,7 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len temp[raw_idx++] = command >> 8; #endif } - // add parameters folllowed by crc + // add parameters followed by crc // skipped if len == 0 for (size_t i = 0; i < data_len; i++) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index 88e1d59984..3f0282a5d4 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -20,13 +20,13 @@ class SensirionI2CDevice : public i2c::I2CDevice { * handles crc check used by Sensirion sensors * @param data pointer to raw result * @param len number of words to read - * @return true if reading succeded + * @return true if reading succeeded */ bool read_data(uint16_t *data, uint8_t len); /** Read 1 data word from i2c device. * @param data reference to raw result - * @return true if reading succeded + * @return true if reading succeeded */ bool read_data(uint16_t &data) { return this->read_data(&data, 1); } @@ -35,8 +35,8 @@ class SensirionI2CDevice : public i2c::I2CDevice { * @param i2c register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c commmand and reading the result - * @return true if reading succeded + * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @return true if reading succeeded */ bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(command, ADDR_16_BIT, data, len, delay); @@ -44,8 +44,8 @@ class SensirionI2CDevice : public i2c::I2CDevice { /** Read 1 data word from 16 bit i2c register. * @param i2c register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c commmand and reading the result - * @return true if reading succeded + * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @return true if reading succeeded */ bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay); @@ -56,8 +56,8 @@ class SensirionI2CDevice : public i2c::I2CDevice { * @param i2c register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c commmand and reading the result - * @return true if reading succeded + * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(i2c_register, ADDR_8_BIT, data, len, delay); @@ -66,8 +66,8 @@ class SensirionI2CDevice : public i2c::I2CDevice { /** Read 1 data word from 8 bit i2c register. * @param i2c register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c commmand and reading the result - * @return true if reading succeded + * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay); @@ -75,21 +75,21 @@ class SensirionI2CDevice : public i2c::I2CDevice { /** Write a command to the i2c device. * @param command i2c command to send - * @return true if reading succeded + * @return true if reading succeeded */ template bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); } /** Write a command and one data word to the i2c device . * @param command i2c command to send * @param data argument for the i2c command - * @return true if reading succeded + * @return true if reading succeeded */ template bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); } /** Write a command with arguments as words * @param i2c_register i2c command to send - an be uint8_t or uint16_t * @param data vector arguments for the i2c command - * @return true if reading succeded + * @return true if reading succeeded */ template bool write_command(T i2c_register, const std::vector &data) { return write_command_(i2c_register, sizeof(T), data.data(), data.size()); @@ -99,7 +99,7 @@ class SensirionI2CDevice : public i2c::I2CDevice { * @param i2c_register i2c command to send - an be uint8_t or uint16_t * @param data arguments for the i2c command * @param len number of arguments (words) - * @return true if reading succeded + * @return true if reading succeeded */ template bool write_command(T i2c_register, const uint16_t *data, uint8_t len) { // limit to 8 or 16 bit only @@ -115,7 +115,7 @@ class SensirionI2CDevice : public i2c::I2CDevice { * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes * @param data arguments for the i2c command * @param data_len number of arguments (words) - * @return true if reading succeded + * @return true if reading succeeded */ bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len); @@ -125,8 +125,8 @@ class SensirionI2CDevice : public i2c::I2CDevice { * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c commmand and reading the result - * @return true if reading succeded + * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @return true if reading succeeded */ bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay); diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index d01a594889..d6ba038057 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -31,12 +31,15 @@ from esphome.const import ( CONF_FORCE_UPDATE, DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATE, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -51,6 +54,7 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, @@ -66,13 +70,16 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATE, DEVICE_CLASS_DURATION, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -87,6 +94,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SULPHUR_DIOXIDE, DEVICE_CLASS_TEMPERATURE, @@ -101,6 +109,7 @@ STATE_CLASSES = { "": StateClasses.STATE_CLASS_NONE, "measurement": StateClasses.STATE_CLASS_MEASUREMENT, "total_increasing": StateClasses.STATE_CLASS_TOTAL_INCREASING, + "total": StateClasses.STATE_CLASS_TOTAL, } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") @@ -606,8 +615,8 @@ async def register_sensor(var, config): await setup_sensor_core_(var, config) -async def new_sensor(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_sensor(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_sensor(var, config) return var diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index d4a7a52fa1..7a2c98109c 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -37,31 +37,38 @@ MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_fi void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional MedianFilter::new_value(float value) { - if (!std::isnan(value)) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value); + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value); if (++this->send_at_ >= this->send_every_) { this->send_at_ = 0; - float median = 0.0f; + float median = NAN; if (!this->queue_.empty()) { - std::deque median_queue = this->queue_; + // Copy queue without NaN values + std::vector median_queue; + for (auto v : this->queue_) { + if (!std::isnan(v)) { + median_queue.push_back(v); + } + } + sort(median_queue.begin(), median_queue.end()); size_t queue_size = median_queue.size(); - if (queue_size % 2) { - median = median_queue[queue_size / 2]; - } else { - median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f; + if (queue_size) { + if (queue_size % 2) { + median = median_queue[queue_size / 2]; + } else { + median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f; + } } } - ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING", this, median); + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING %f", this, value, median); return median; } return {}; @@ -74,29 +81,36 @@ void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = sen void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; } optional QuantileFilter::new_value(float value) { - if (!std::isnan(value)) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_); + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_); if (++this->send_at_ >= this->send_every_) { this->send_at_ = 0; - float result = 0.0f; + float result = NAN; if (!this->queue_.empty()) { - std::deque quantile_queue = this->queue_; + // Copy queue without NaN values + std::vector quantile_queue; + for (auto v : this->queue_) { + if (!std::isnan(v)) { + quantile_queue.push_back(v); + } + } + sort(quantile_queue.begin(), quantile_queue.end()); size_t queue_size = quantile_queue.size(); - size_t position = ceilf(queue_size * this->quantile_) - 1; - ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position, queue_size); - result = quantile_queue[position]; + if (queue_size) { + size_t position = ceilf(queue_size * this->quantile_) - 1; + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position + 1, queue_size); + result = quantile_queue[position]; + } } - ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING", this, result); + ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING %f", this, value, result); return result; } return {}; @@ -108,24 +122,23 @@ MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional MinFilter::new_value(float value) { - if (!std::isnan(value)) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value); + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value); if (++this->send_at_ >= this->send_every_) { this->send_at_ = 0; - float min = 0.0f; - if (!this->queue_.empty()) { - std::deque::iterator it = std::min_element(queue_.begin(), queue_.end()); - min = *it; + float min = NAN; + for (auto v : this->queue_) { + if (!std::isnan(v)) { + min = std::isnan(min) ? v : std::min(min, v); + } } - ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING", this, min); + ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING %f", this, value, min); return min; } return {}; @@ -137,24 +150,23 @@ MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional MaxFilter::new_value(float value) { - if (!std::isnan(value)) { - while (this->queue_.size() >= this->window_size_) { - this->queue_.pop_front(); - } - this->queue_.push_back(value); - ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value); + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value); if (++this->send_at_ >= this->send_every_) { this->send_at_ = 0; - float max = 0.0f; - if (!this->queue_.empty()) { - std::deque::iterator it = std::max_element(queue_.begin(), queue_.end()); - max = *it; + float max = NAN; + for (auto v : this->queue_) { + if (!std::isnan(v)) { + max = std::isnan(max) ? v : std::max(max, v); + } } - ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING", this, max); + ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING %f", this, value, max); return max; } return {}; @@ -167,33 +179,30 @@ SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window void SlidingWindowMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } optional SlidingWindowMovingAverageFilter::new_value(float value) { - if (!std::isnan(value)) { - if (this->queue_.size() == this->window_size_) { - this->sum_ -= this->queue_[0]; - this->queue_.pop_front(); - } - this->queue_.push_back(value); - this->sum_ += value; + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); } - float average; - if (this->queue_.empty()) { - average = 0.0f; - } else { - average = this->sum_ / this->queue_.size(); - } - ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) -> %f", this, value, average); + this->queue_.push_back(value); + ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f)", this, value); - if (++this->send_at_ % this->send_every_ == 0) { - if (this->send_at_ >= 10000) { - // Recalculate to prevent floating point error accumulating - this->sum_ = 0; - for (auto v : this->queue_) - this->sum_ += v; - average = this->sum_ / this->queue_.size(); - this->send_at_ = 0; + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float sum = 0; + size_t valid_count = 0; + for (auto v : this->queue_) { + if (!std::isnan(v)) { + sum += v; + valid_count++; + } } - ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING", this, value); + float average = NAN; + if (valid_count) { + average = sum / valid_count; + } + + ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING %f", this, value, average); return average; } return {}; @@ -206,17 +215,17 @@ optional ExponentialMovingAverageFilter::new_value(float value) { if (!std::isnan(value)) { if (this->first_value_) { this->accumulator_ = value; + this->first_value_ = false; } else { this->accumulator_ = (this->alpha_ * value) + (1.0f - this->alpha_) * this->accumulator_; } - this->first_value_ = false; } - float average = this->accumulator_; + const float average = std::isnan(value) ? value : this->accumulator_; ESP_LOGVV(TAG, "ExponentialMovingAverageFilter(%p)::new_value(%f) -> %f", this, value, average); if (++this->send_at_ >= this->send_every_) { - ESP_LOGVV(TAG, "ExponentialMovingAverageFilter(%p)::new_value(%f) SENDING", this, value); + ESP_LOGVV(TAG, "ExponentialMovingAverageFilter(%p)::new_value(%f) SENDING %f", this, value, average); this->send_at_ = 0; return average; } @@ -308,8 +317,13 @@ optional ThrottleFilter::new_value(float value) { // DeltaFilter DeltaFilter::DeltaFilter(float min_delta) : min_delta_(min_delta), last_value_(NAN) {} optional DeltaFilter::new_value(float value) { - if (std::isnan(value)) - return {}; + if (std::isnan(value)) { + if (std::isnan(this->last_value_)) { + return {}; + } else { + return this->last_value_ = value; + } + } if (std::isnan(this->last_value_)) { return this->last_value_ = value; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index a39c1ba25a..6344d34661 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -180,7 +180,6 @@ class SlidingWindowMovingAverageFilter : public Filter { void set_window_size(size_t window_size); protected: - float sum_{0.0}; std::deque queue_; size_t send_every_; size_t send_at_; @@ -203,7 +202,7 @@ class ExponentialMovingAverageFilter : public Filter { protected: bool first_value_{true}; - float accumulator_{0.0f}; + float accumulator_{NAN}; size_t send_every_; size_t send_at_; float alpha_; diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index c0869631aa..a729791e7e 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -12,6 +12,8 @@ std::string state_class_to_string(StateClass state_class) { return "measurement"; case STATE_CLASS_TOTAL_INCREASING: return "total_increasing"; + case STATE_CLASS_TOTAL: + return "total"; case STATE_CLASS_NONE: default: return ""; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f29125ee42..ba9edd68d0 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -36,6 +36,7 @@ enum StateClass : uint8_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, + STATE_CLASS_TOTAL = 3, }; std::string state_class_to_string(StateClass state_class); diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 257c30075f..7fe46b9518 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -39,7 +39,7 @@ void SGP4xComponent::setup() { ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected"); // disable the sensor this->nox_sensor_->set_disabled_by_default(true); - // make sure it's not visiable in HA + // make sure it's not visible in HA this->nox_sensor_->set_internal(true); this->nox_sensor_->state = NAN; // remove pointer to sensor @@ -104,8 +104,8 @@ void SGP4xComponent::setup() { https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight timing variations so the software timer should be accurate enough for this. - This block starts sampling from the sensor at 1Hz, and is done seperately from the call - to the update method. This seperation is to support getting accurate measurements but + This block starts sampling from the sensor at 1Hz, and is done separately from the call + to the update method. This separation is to support getting accurate measurements but limit the amount of communication done over wifi for power consumption or to keep the number of records reported from being overwhelming. */ @@ -236,9 +236,9 @@ bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) { } uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); - // first paramater are the relative humidity ticks + // first parameter are the relative humidity ticks data[0] = rhticks; - // secomd paramater are the temperature ticks + // secomd parameter are the temperature ticks data[1] = tempticks; if (!this->write_command(command, data, 2)) { diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 32c556da5e..94fe836742 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -49,7 +49,7 @@ constexpr float POWER_SCALING_FACTOR = 880373; constexpr float VOLTAGE_SCALING_FACTOR = 347800; constexpr float CURRENT_SCALING_FACTOR = 1448; -// Esentially std::size() for pre c++17 +// Essentially std::size() for pre c++17 template constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; } } // Anonymous namespace diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 9cfeb54153..6af0283483 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -42,12 +42,12 @@ void SlowPWMOutput::loop() { uint32_t now = millis(); float scaled_state = this->state_ * this->period_; - if (now >= this->period_start_time_ + this->period_) { + if (now - this->period_start_time_ >= this->period_) { ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state); this->period_start_time_ += this->period_; } - this->set_output_state_(now < this->period_start_time_ + scaled_state); + this->set_output_state_(scaled_state > now - this->period_start_time_); } void SlowPWMOutput::dump_config() { diff --git a/esphome/components/smt100/__init__.py b/esphome/components/smt100/__init__.py new file mode 100644 index 0000000000..44ed591400 --- /dev/null +++ b/esphome/components/smt100/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@piechade"] diff --git a/esphome/components/smt100/sensor.py b/esphome/components/smt100/sensor.py new file mode 100644 index 0000000000..33eb78b841 --- /dev/null +++ b/esphome/components/smt100/sensor.py @@ -0,0 +1,94 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart + +from esphome.const import ( + CONF_ID, + CONF_COUNTS, + CONF_DIELECTRIC_CONSTANT, + CONF_TEMPERATURE, + CONF_MOISTURE, + CONF_VOLTAGE, + ICON_WATER_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_EMPTY, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +DEPENDENCIES = ["uart"] + +smt100_ns = cg.esphome_ns.namespace("smt100") +SMT100 = smt100_ns.class_("SMT100Component", cg.PollingComponent, uart.UARTDevice) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SMT100), + cv.Optional(CONF_COUNTS): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DIELECTRIC_CONSTANT): sensor.sensor_schema( + unit_of_measurement=UNIT_EMPTY, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "smt100", baud_rate=9600, require_rx=True, require_tx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_COUNTS in config: + sens = await sensor.new_sensor(config[CONF_COUNTS]) + cg.add(var.set_counts_sensor(sens)) + + if CONF_DIELECTRIC_CONSTANT in config: + sens = await sensor.new_sensor(config[CONF_DIELECTRIC_CONSTANT]) + cg.add(var.set_dielectric_constant_sensor(sens)) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_MOISTURE in config: + sens = await sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture_sensor(sens)) + + if CONF_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(sens)) diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp new file mode 100644 index 0000000000..24ba05b894 --- /dev/null +++ b/esphome/components/smt100/smt100.cpp @@ -0,0 +1,84 @@ +#include "smt100.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace smt100 { + +static const char *const TAG = "smt100"; + +void SMT100Component::update() { + ESP_LOGV(TAG, "Sending measurement request"); + this->write_str("GetAllMeasurements!\r"); +} + +void SMT100Component::loop() { + static char buffer[MAX_LINE_LENGTH]; + while (this->available() != 0) { + if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { + int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); + float dielectric_constant = (float) strtod((strtok(nullptr, ",")), nullptr); + float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); + float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); + float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); + + if (this->counts_sensor_ != nullptr) { + counts_sensor_->publish_state(counts); + } + + if (this->dielectric_constant_sensor_ != nullptr) { + dielectric_constant_sensor_->publish_state(dielectric_constant); + } + + if (this->moisture_sensor_ != nullptr) { + moisture_sensor_->publish_state(moisture); + } + + if (this->temperature_sensor_ != nullptr) { + temperature_sensor_->publish_state(temperature); + } + + if (this->voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(voltage); + } + } + } +} + +float SMT100Component::get_setup_priority() const { return setup_priority::DATA; } + +void SMT100Component::dump_config() { + ESP_LOGCONFIG(TAG, "SMT100:"); + + LOG_SENSOR(TAG, "Counts", this->temperature_sensor_); + LOG_SENSOR(TAG, "Dielectric Constant", this->temperature_sensor_); + LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_); + LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_); + LOG_UPDATE_INTERVAL(this); + this->check_uart_settings(9600); +} + +int SMT100Component::readline_(int readch, char *buffer, int len) { + static int pos = 0; + int rpos; + + if (readch > 0) { + switch (readch) { + case '\n': // Ignore new-lines + break; + case '\r': // Return on CR + rpos = pos; + pos = 0; // Reset position index ready for next time + return rpos; + default: + if (pos < len - 1) { + buffer[pos++] = readch; + buffer[pos] = 0; + } + } + } + // No end of line has been found, so return -1. + return -1; +} + +} // namespace smt100 +} // namespace esphome diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h new file mode 100644 index 0000000000..017818bdcf --- /dev/null +++ b/esphome/components/smt100/smt100.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace smt100 { + +class SMT100Component : public PollingComponent, public uart::UARTDevice { + static const uint16_t MAX_LINE_LENGTH = 31; + + public: + SMT100Component() = default; + + void dump_config() override; + void loop() override; + void update() override; + + float get_setup_priority() const override; + + void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } + void set_dielectric_constant_sensor(sensor::Sensor *dielectric_constant_sensor) { + this->dielectric_constant_sensor_ = dielectric_constant_sensor; + } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_moisture_sensor(sensor::Sensor *moisture_sensor) { this->moisture_sensor_ = moisture_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + + protected: + int readline_(int readch, char *buffer, int len); + + sensor::Sensor *counts_sensor_{nullptr}; + sensor::Sensor *dielectric_constant_sensor_{nullptr}; + sensor::Sensor *moisture_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + + uint32_t last_transmission_{0}; +}; + +} // namespace smt100 +} // namespace esphome diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index d07f9229b6..dc6b719f1b 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -161,7 +161,7 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a return false; } if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) { - ESP_LOGW(TAG, "[%04d] Payload length field does not match packet lenght (%d, expected %d)", this->write_count_, + ESP_LOGW(TAG, "[%04d] Payload length field does not match packet length (%d, expected %d)", this->write_count_, cmd[5], len - 7); return false; } diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py new file mode 100644 index 0000000000..659eb5b58e --- /dev/null +++ b/esphome/components/sprinkler/__init__.py @@ -0,0 +1,599 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.components import switch +from esphome.const import ( + CONF_ID, + CONF_NAME, + CONF_REPEAT, + CONF_RUN_DURATION, +) + +AUTO_LOAD = ["switch"] +CODEOWNERS = ["@kbx81"] + +CONF_AUTO_ADVANCE_SWITCH = "auto_advance_switch" +CONF_ENABLE_SWITCH = "enable_switch" +CONF_MAIN_SWITCH = "main_switch" +CONF_MANUAL_SELECTION_DELAY = "manual_selection_delay" +CONF_MULTIPLIER = "multiplier" +CONF_PUMP_OFF_SWITCH_ID = "pump_off_switch_id" +CONF_PUMP_ON_SWITCH_ID = "pump_on_switch_id" +CONF_PUMP_PULSE_DURATION = "pump_pulse_duration" +CONF_PUMP_START_PUMP_DELAY = "pump_start_pump_delay" +CONF_PUMP_START_VALVE_DELAY = "pump_start_valve_delay" +CONF_PUMP_STOP_PUMP_DELAY = "pump_stop_pump_delay" +CONF_PUMP_STOP_VALVE_DELAY = "pump_stop_valve_delay" +CONF_PUMP_SWITCH = "pump_switch" +CONF_PUMP_SWITCH_ID = "pump_switch_id" +CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY = "pump_switch_off_during_valve_open_delay" +CONF_QUEUE_ENABLE_SWITCH = "queue_enable_switch" +CONF_REVERSE_SWITCH = "reverse_switch" +CONF_VALVE_NUMBER = "valve_number" +CONF_VALVE_OPEN_DELAY = "valve_open_delay" +CONF_VALVE_OVERLAP = "valve_overlap" +CONF_VALVE_PULSE_DURATION = "valve_pulse_duration" +CONF_VALVE_OFF_SWITCH_ID = "valve_off_switch_id" +CONF_VALVE_ON_SWITCH_ID = "valve_on_switch_id" +CONF_VALVE_SWITCH = "valve_switch" +CONF_VALVE_SWITCH_ID = "valve_switch_id" +CONF_VALVES = "valves" + +sprinkler_ns = cg.esphome_ns.namespace("sprinkler") +Sprinkler = sprinkler_ns.class_("Sprinkler", cg.Component) +SprinklerControllerSwitch = sprinkler_ns.class_( + "SprinklerControllerSwitch", switch.Switch, cg.Component +) + +SetMultiplierAction = sprinkler_ns.class_("SetMultiplierAction", automation.Action) +QueueValveAction = sprinkler_ns.class_("QueueValveAction", automation.Action) +ClearQueuedValvesAction = sprinkler_ns.class_( + "ClearQueuedValvesAction", automation.Action +) +SetRepeatAction = sprinkler_ns.class_("SetRepeatAction", automation.Action) +SetRunDurationAction = sprinkler_ns.class_("SetRunDurationAction", automation.Action) +StartFromQueueAction = sprinkler_ns.class_("StartFromQueueAction", automation.Action) +StartFullCycleAction = sprinkler_ns.class_("StartFullCycleAction", automation.Action) +StartSingleValveAction = sprinkler_ns.class_( + "StartSingleValveAction", automation.Action +) +ShutdownAction = sprinkler_ns.class_("ShutdownAction", automation.Action) +NextValveAction = sprinkler_ns.class_("NextValveAction", automation.Action) +PreviousValveAction = sprinkler_ns.class_("PreviousValveAction", automation.Action) +PauseAction = sprinkler_ns.class_("PauseAction", automation.Action) +ResumeAction = sprinkler_ns.class_("ResumeAction", automation.Action) +ResumeOrStartAction = sprinkler_ns.class_("ResumeOrStartAction", automation.Action) + + +def validate_sprinkler(config): + for sprinkler_controller_index, sprinkler_controller in enumerate(config): + if len(sprinkler_controller[CONF_VALVES]) <= 1: + exclusions = [ + CONF_VALVE_OPEN_DELAY, + CONF_VALVE_OVERLAP, + CONF_AUTO_ADVANCE_SWITCH, + CONF_MAIN_SWITCH, + CONF_REVERSE_SWITCH, + ] + for config_item in exclusions: + if config_item in sprinkler_controller: + raise cv.Invalid(f"Do not define {config_item} with only one valve") + if CONF_ENABLE_SWITCH in sprinkler_controller[CONF_VALVES][0]: + raise cv.Invalid( + f"Do not define {CONF_ENABLE_SWITCH} with only one valve" + ) + else: + requirements = [ + CONF_AUTO_ADVANCE_SWITCH, + CONF_MAIN_SWITCH, + ] + for config_item in requirements: + if config_item not in sprinkler_controller: + raise cv.Invalid( + f"{config_item} is a required option for {sprinkler_controller_index}" + ) + + if ( + CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller + and CONF_VALVE_OPEN_DELAY not in sprinkler_controller + ): + if sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY]: + raise cv.Invalid( + f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" + ) + + for valve in sprinkler_controller[CONF_VALVES]: + if ( + CONF_VALVE_OVERLAP in sprinkler_controller + and valve[CONF_RUN_DURATION] <= sprinkler_controller[CONF_VALVE_OVERLAP] + ): + raise cv.Invalid( + f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OVERLAP}" + ) + if ( + CONF_VALVE_OPEN_DELAY in sprinkler_controller + and valve[CONF_RUN_DURATION] + <= sprinkler_controller[CONF_VALVE_OPEN_DELAY] + ): + raise cv.Invalid( + f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OPEN_DELAY}" + ) + if ( + CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID not in valve + ) or ( + CONF_PUMP_ON_SWITCH_ID in valve and CONF_PUMP_OFF_SWITCH_ID not in valve + ): + raise cv.Invalid( + f"Both {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID} must be specified for latching pump configuration" + ) + if CONF_PUMP_SWITCH_ID in valve and ( + CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"Do not specify {CONF_PUMP_OFF_SWITCH_ID} or {CONF_PUMP_ON_SWITCH_ID} when using {CONF_PUMP_SWITCH_ID}" + ) + if CONF_PUMP_PULSE_DURATION not in sprinkler_controller and ( + CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"{CONF_PUMP_PULSE_DURATION} must be specified when using {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID}" + ) + if ( + CONF_VALVE_OFF_SWITCH_ID in valve + and CONF_VALVE_ON_SWITCH_ID not in valve + ) or ( + CONF_VALVE_ON_SWITCH_ID in valve + and CONF_VALVE_OFF_SWITCH_ID not in valve + ): + raise cv.Invalid( + f"Both {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified for latching valve configuration" + ) + if CONF_VALVE_SWITCH_ID in valve and ( + CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"Do not specify {CONF_VALVE_OFF_SWITCH_ID} or {CONF_VALVE_ON_SWITCH_ID} when using {CONF_VALVE_SWITCH_ID}" + ) + if CONF_VALVE_PULSE_DURATION not in sprinkler_controller and ( + CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve + ): + raise cv.Invalid( + f"{CONF_VALVE_PULSE_DURATION} must be specified when using {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID}" + ) + if ( + CONF_VALVE_SWITCH_ID not in valve + and CONF_VALVE_OFF_SWITCH_ID not in valve + and CONF_VALVE_ON_SWITCH_ID not in valve + ): + raise cv.Invalid( + f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration" + ) + return config + + +SPRINKLER_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Sprinkler), + } +) + +SPRINKLER_ACTION_REPEAT_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_REPEAT): cv.templatable(cv.positive_int), + }, + key=CONF_REPEAT, +) + +SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), + }, + key=CONF_VALVE_NUMBER, +) + +SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Sprinkler), + cv.Required(CONF_MULTIPLIER): cv.templatable(cv.positive_float), + }, + key=CONF_MULTIPLIER, +) + +SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Sprinkler), + cv.Required(CONF_RUN_DURATION): cv.templatable(cv.positive_time_period_seconds), + cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), + } +) + +SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Sprinkler), + cv.Optional(CONF_RUN_DURATION, default=0): cv.templatable( + cv.positive_time_period_seconds + ), + cv.Required(CONF_VALVE_NUMBER): cv.templatable(cv.positive_int), + } +) + +SPRINKLER_VALVE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ENABLE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch), + cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch), + cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), + cv.Required(CONF_RUN_DURATION): cv.positive_time_period_seconds, + cv.Required(CONF_VALVE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.use_id(switch.Switch), + cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.use_id(switch.Switch), + cv.Optional(CONF_VALVE_SWITCH_ID): cv.use_id(switch.Switch), + } +) + +SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sprinkler), + cv.Optional(CONF_AUTO_ADVANCE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_MAIN_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_QUEUE_ENABLE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_REVERSE_SWITCH): cv.maybe_simple_value( + switch.SWITCH_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SprinklerControllerSwitch), + } + ) + ), + key=CONF_NAME, + ), + cv.Optional(CONF_MANUAL_SELECTION_DELAY): cv.positive_time_period_seconds, + cv.Optional(CONF_REPEAT): cv.positive_int, + cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, + cv.Exclusive( + CONF_PUMP_START_PUMP_DELAY, "pump_start_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_PUMP_STOP_PUMP_DELAY, "pump_stop_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY): cv.boolean, + cv.Exclusive( + CONF_PUMP_START_VALVE_DELAY, "pump_start_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_PUMP_STOP_VALVE_DELAY, "pump_stop_xxxx_delay" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_VALVE_OVERLAP, "open_delay/overlap" + ): cv.positive_time_period_seconds, + cv.Exclusive( + CONF_VALVE_OPEN_DELAY, "open_delay/overlap" + ): cv.positive_time_period_seconds, + cv.Required(CONF_VALVES): cv.ensure_list(SPRINKLER_VALVE_SCHEMA), + } +).extend(cv.ENTITY_BASE_SCHEMA) + +CONFIG_SCHEMA = cv.All( + cv.ensure_list(SPRINKLER_CONTROLLER_SCHEMA), + validate_sprinkler, +) + + +@automation.register_action( + "sprinkler.set_multiplier", + SetMultiplierAction, + SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA, +) +async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_MULTIPLIER], args, cg.float_) + cg.add(var.set_multiplier(template_)) + return var + + +@automation.register_action( + "sprinkler.queue_valve", + QueueValveAction, + SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA, +) +async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) + cg.add(var.set_valve_number(template_)) + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) + return var + + +@automation.register_action( + "sprinkler.set_repeat", + SetRepeatAction, + SPRINKLER_ACTION_REPEAT_SCHEMA, +) +async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_REPEAT], args, cg.float_) + cg.add(var.set_repeat(template_)) + return var + + +@automation.register_action( + "sprinkler.set_valve_run_duration", + SetRunDurationAction, + SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA, +) +async def sprinkler_set_valve_run_duration_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) + cg.add(var.set_valve_number(template_)) + template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) + cg.add(var.set_valve_run_duration(template_)) + return var + + +@automation.register_action( + "sprinkler.start_from_queue", StartFromQueueAction, SPRINKLER_ACTION_SCHEMA +) +async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "sprinkler.start_full_cycle", StartFullCycleAction, SPRINKLER_ACTION_SCHEMA +) +async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "sprinkler.start_single_valve", + StartSingleValveAction, + SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA, +) +async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) + cg.add(var.set_valve_to_start(template_)) + return var + + +@automation.register_action( + "sprinkler.clear_queued_valves", ClearQueuedValvesAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action( + "sprinkler.next_valve", NextValveAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action( + "sprinkler.previous_valve", PreviousValveAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action("sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA) +@automation.register_action("sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA) +@automation.register_action( + "sprinkler.resume_or_start_full_cycle", ResumeOrStartAction, SPRINKLER_ACTION_SCHEMA +) +@automation.register_action( + "sprinkler.shutdown", ShutdownAction, SPRINKLER_ACTION_SCHEMA +) +async def sprinkler_simple_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + for sprinkler_controller in config: + if len(sprinkler_controller[CONF_VALVES]) > 1: + var = cg.new_Pvariable( + sprinkler_controller[CONF_ID], + sprinkler_controller[CONF_MAIN_SWITCH][CONF_NAME], + ) + else: + var = cg.new_Pvariable( + sprinkler_controller[CONF_ID], + sprinkler_controller[CONF_VALVES][0][CONF_VALVE_SWITCH][CONF_NAME], + ) + await cg.register_component(var, sprinkler_controller) + + if len(sprinkler_controller[CONF_VALVES]) > 1: + sw_var = await switch.new_switch(sprinkler_controller[CONF_MAIN_SWITCH]) + await cg.register_component(sw_var, sprinkler_controller[CONF_MAIN_SWITCH]) + cg.add(var.set_controller_main_switch(sw_var)) + + sw_aa_var = await switch.new_switch( + sprinkler_controller[CONF_AUTO_ADVANCE_SWITCH] + ) + await cg.register_component( + sw_aa_var, sprinkler_controller[CONF_AUTO_ADVANCE_SWITCH] + ) + cg.add(var.set_controller_auto_adv_switch(sw_aa_var)) + + if CONF_QUEUE_ENABLE_SWITCH in sprinkler_controller: + sw_qen_var = await switch.new_switch( + sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH] + ) + await cg.register_component( + sw_qen_var, sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH] + ) + cg.add(var.set_controller_queue_enable_switch(sw_qen_var)) + + if CONF_REVERSE_SWITCH in sprinkler_controller: + sw_rev_var = await switch.new_switch( + sprinkler_controller[CONF_REVERSE_SWITCH] + ) + await cg.register_component( + sw_rev_var, sprinkler_controller[CONF_REVERSE_SWITCH] + ) + cg.add(var.set_controller_reverse_switch(sw_rev_var)) + + for valve in sprinkler_controller[CONF_VALVES]: + sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH]) + await cg.register_component(sw_valve_var, valve[CONF_VALVE_SWITCH]) + + if ( + CONF_ENABLE_SWITCH in valve + and len(sprinkler_controller[CONF_VALVES]) > 1 + ): + sw_en_var = await switch.new_switch(valve[CONF_ENABLE_SWITCH]) + await cg.register_component(sw_en_var, valve[CONF_ENABLE_SWITCH]) + + cg.add(var.add_valve(sw_valve_var, sw_en_var)) + else: + cg.add(var.add_valve(sw_valve_var)) + + if CONF_MANUAL_SELECTION_DELAY in sprinkler_controller: + cg.add( + var.set_manual_selection_delay( + sprinkler_controller[CONF_MANUAL_SELECTION_DELAY] + ) + ) + + if CONF_REPEAT in sprinkler_controller: + cg.add(var.set_repeat(sprinkler_controller[CONF_REPEAT])) + + if CONF_VALVE_OVERLAP in sprinkler_controller: + cg.add(var.set_valve_overlap(sprinkler_controller[CONF_VALVE_OVERLAP])) + + if CONF_VALVE_OPEN_DELAY in sprinkler_controller: + cg.add( + var.set_valve_open_delay(sprinkler_controller[CONF_VALVE_OPEN_DELAY]) + ) + + if CONF_PUMP_START_PUMP_DELAY in sprinkler_controller: + cg.add( + var.set_pump_start_delay( + sprinkler_controller[CONF_PUMP_START_PUMP_DELAY] + ) + ) + + if CONF_PUMP_STOP_PUMP_DELAY in sprinkler_controller: + cg.add( + var.set_pump_stop_delay(sprinkler_controller[CONF_PUMP_STOP_PUMP_DELAY]) + ) + + if CONF_PUMP_START_VALVE_DELAY in sprinkler_controller: + cg.add( + var.set_valve_start_delay( + sprinkler_controller[CONF_PUMP_START_VALVE_DELAY] + ) + ) + + if CONF_PUMP_STOP_VALVE_DELAY in sprinkler_controller: + cg.add( + var.set_valve_stop_delay( + sprinkler_controller[CONF_PUMP_STOP_VALVE_DELAY] + ) + ) + + if CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller: + cg.add( + var.set_pump_switch_off_during_valve_open_delay( + sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY] + ) + ) + + for sprinkler_controller in config: + var = await cg.get_variable(sprinkler_controller[CONF_ID]) + for valve_index, valve in enumerate(sprinkler_controller[CONF_VALVES]): + if CONF_VALVE_SWITCH_ID in valve: + valve_switch = await cg.get_variable(valve[CONF_VALVE_SWITCH_ID]) + cg.add( + var.configure_valve_switch( + valve_index, valve_switch, valve[CONF_RUN_DURATION] + ) + ) + elif CONF_VALVE_OFF_SWITCH_ID in valve and CONF_VALVE_ON_SWITCH_ID in valve: + valve_switch_off = await cg.get_variable( + valve[CONF_VALVE_OFF_SWITCH_ID] + ) + valve_switch_on = await cg.get_variable(valve[CONF_VALVE_ON_SWITCH_ID]) + cg.add( + var.configure_valve_switch_pulsed( + valve_index, + valve_switch_off, + valve_switch_on, + sprinkler_controller[CONF_VALVE_PULSE_DURATION], + valve[CONF_RUN_DURATION], + ) + ) + + if CONF_PUMP_SWITCH_ID in valve: + pump = await cg.get_variable(valve[CONF_PUMP_SWITCH_ID]) + cg.add(var.configure_valve_pump_switch(valve_index, pump)) + elif CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID in valve: + pump_off = await cg.get_variable(valve[CONF_PUMP_OFF_SWITCH_ID]) + pump_on = await cg.get_variable(valve[CONF_PUMP_ON_SWITCH_ID]) + cg.add( + var.configure_valve_pump_switch_pulsed( + valve_index, + pump_off, + pump_on, + sprinkler_controller[CONF_PUMP_PULSE_DURATION], + ) + ) + + for sprinkler_controller in config: + var = await cg.get_variable(sprinkler_controller[CONF_ID]) + for controller_to_add in config: + if sprinkler_controller[CONF_ID] != controller_to_add[CONF_ID]: + cg.add( + var.add_controller( + await cg.get_variable(controller_to_add[CONF_ID]) + ) + ) diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h new file mode 100644 index 0000000000..dd0ea44633 --- /dev/null +++ b/esphome/components/sprinkler/automation.h @@ -0,0 +1,169 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/sprinkler/sprinkler.h" + +namespace esphome { +namespace sprinkler { + +template class SetMultiplierAction : public Action { + public: + explicit SetMultiplierAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(float, multiplier) + + void play(Ts... x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + +template class QueueValveAction : public Action { + public: + explicit QueueValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(size_t, valve_number) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) + + void play(Ts... x) override { + this->sprinkler_->queue_valve(this->valve_number_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } + + protected: + Sprinkler *sprinkler_; +}; + +template class ClearQueuedValvesAction : public Action { + public: + explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->clear_queued_valves(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class SetRepeatAction : public Action { + public: + explicit SetRepeatAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(uint32_t, repeat) + + void play(Ts... x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + +template class SetRunDurationAction : public Action { + public: + explicit SetRunDurationAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(size_t, valve_number) + TEMPLATABLE_VALUE(uint32_t, valve_run_duration) + + void play(Ts... x) override { + this->sprinkler_->set_valve_run_duration(this->valve_number_.optional_value(x...), + this->valve_run_duration_.optional_value(x...)); + } + + protected: + Sprinkler *sprinkler_; +}; + +template class StartFromQueueAction : public Action { + public: + explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->start_from_queue(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class StartFullCycleAction : public Action { + public: + explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->start_full_cycle(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class StartSingleValveAction : public Action { + public: + explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + TEMPLATABLE_VALUE(size_t, valve_to_start) + + void play(Ts... x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...)); } + + protected: + Sprinkler *sprinkler_; +}; + +template class ShutdownAction : public Action { + public: + explicit ShutdownAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->shutdown(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class NextValveAction : public Action { + public: + explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->next_valve(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class PreviousValveAction : public Action { + public: + explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->previous_valve(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class PauseAction : public Action { + public: + explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->pause(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class ResumeAction : public Action { + public: + explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->resume(); } + + protected: + Sprinkler *sprinkler_; +}; + +template class ResumeOrStartAction : public Action { + public: + explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} + + void play(Ts... x) override { this->sprinkler_->resume_or_start_full_cycle(); } + + protected: + Sprinkler *sprinkler_; +}; + +} // namespace sprinkler +} // namespace esphome diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp new file mode 100644 index 0000000000..ab694c8412 --- /dev/null +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -0,0 +1,1347 @@ +#include "automation.h" +#include "sprinkler.h" + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace sprinkler { + +static const char *const TAG = "sprinkler"; + +SprinklerSwitch::SprinklerSwitch() {} +SprinklerSwitch::SprinklerSwitch(switch_::Switch *sprinkler_switch) : on_switch_(sprinkler_switch) {} +SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration) + : pulse_duration_(pulse_duration), off_switch_(off_switch), on_switch_(on_switch) {} + +bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } + +void SprinklerSwitch::loop() { + if ((this->pinned_millis_) && (millis() > this->pinned_millis_ + this->pulse_duration_)) { + this->pinned_millis_ = 0; // reset tracker + if (this->off_switch_->state) { + this->off_switch_->turn_off(); + } + if (this->on_switch_->state) { + this->on_switch_->turn_off(); + } + } +} + +void SprinklerSwitch::turn_off() { + if (!this->state()) { // do nothing if we're already in the requested state + return; + } + if (this->off_switch_ != nullptr) { // latching valve, start a pulse + if (!this->off_switch_->state) { + this->off_switch_->turn_on(); + } + this->pinned_millis_ = millis(); + } else if (this->on_switch_ != nullptr) { // non-latching valve + this->on_switch_->turn_off(); + } + this->state_ = false; +} + +void SprinklerSwitch::turn_on() { + if (this->state()) { // do nothing if we're already in the requested state + return; + } + if (this->off_switch_ != nullptr) { // latching valve, start a pulse + if (!this->on_switch_->state) { + this->on_switch_->turn_on(); + } + this->pinned_millis_ = millis(); + } else if (this->on_switch_ != nullptr) { // non-latching valve + this->on_switch_->turn_on(); + } + this->state_ = true; +} + +bool SprinklerSwitch::state() { + if ((this->off_switch_ == nullptr) && (this->on_switch_ != nullptr)) { // latching valve is not configured... + return this->on_switch_->state; // ...so just return the pump switch state + } + return this->state_; +} + +void SprinklerSwitch::sync_valve_state(bool latch_state) { + if (this->is_latching_valve()) { + this->state_ = latch_state; + } else if (this->on_switch_ != nullptr) { + this->state_ = this->on_switch_->state; + } +} + +SprinklerControllerSwitch::SprinklerControllerSwitch() + : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} + +void SprinklerControllerSwitch::loop() { + if (!this->f_.has_value()) + return; + auto s = (*this->f_)(); + if (!s.has_value()) + return; + + this->publish_state(*s); +} + +void SprinklerControllerSwitch::write_state(bool state) { + if (this->prev_trigger_ != nullptr) { + this->prev_trigger_->stop_action(); + } + + if (state) { + this->prev_trigger_ = this->turn_on_trigger_; + this->turn_on_trigger_->trigger(); + } else { + this->prev_trigger_ = this->turn_off_trigger_; + this->turn_off_trigger_->trigger(); + } + + if (this->optimistic_) + this->publish_state(state); +} + +void SprinklerControllerSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } +bool SprinklerControllerSwitch::assumed_state() { return this->assumed_state_; } +void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } +float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } + +Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } +Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } + +void SprinklerControllerSwitch::setup() { + if (!this->restore_state_) + return; + + auto restored = this->get_initial_state(); + if (!restored.has_value()) + return; + + ESP_LOGD(TAG, " Restored state %s", ONOFF(*restored)); + if (*restored) { + this->turn_on(); + } else { + this->turn_off(); + } +} + +void SprinklerControllerSwitch::dump_config() { + LOG_SWITCH("", "Sprinkler Switch", this); + ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +void SprinklerControllerSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } + +void SprinklerControllerSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } + +SprinklerValveOperator::SprinklerValveOperator() {} +SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller) + : controller_(controller), valve_(valve) {} + +void SprinklerValveOperator::loop() { + if (millis() >= this->pinned_millis_) { // dummy check + switch (this->state_) { + case STARTING: + if (millis() > (this->pinned_millis_ + this->start_delay_)) { + this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state + } + break; + + case ACTIVE: + if (millis() > (this->pinned_millis_ + this->start_delay_ + this->run_duration_)) { + this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down + } + break; + + case STOPPING: + if (millis() > (this->pinned_millis_ + this->stop_delay_)) { + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + } + break; + + default: + break; + } + } else { // perhaps millis() rolled over...or something else is horribly wrong! + this->stop(); // bail out (TODO: handle this highly unlikely situation better...) + } +} + +void SprinklerValveOperator::set_controller(Sprinkler *controller) { + if (controller != nullptr) { + this->controller_ = controller; + } +} + +void SprinklerValveOperator::set_valve(SprinklerValve *valve) { + if (valve != nullptr) { + this->state_ = IDLE; // reset state + this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it + this->pinned_millis_ = 0; // reset because (new) valve has not been started yet + this->kill_(); // ensure everything is off before we let go! + this->valve_ = valve; // finally, set the pointer to the new valve + } +} + +void SprinklerValveOperator::set_run_duration(uint32_t run_duration) { + if (run_duration) { + this->run_duration_ = run_duration * 1000; + } +} + +void SprinklerValveOperator::set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay) { + this->start_delay_is_valve_delay_ = start_delay_is_valve_delay; + this->start_delay_ = start_delay * 1000; // because 1000 milliseconds is one second +} + +void SprinklerValveOperator::set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay) { + this->stop_delay_is_valve_delay_ = stop_delay_is_valve_delay; + this->stop_delay_ = stop_delay * 1000; // because 1000 milliseconds is one second +} + +void SprinklerValveOperator::start() { + if (!this->run_duration_) { // can't start if zero run duration + return; + } + if (this->start_delay_ && (this->pump_switch() != nullptr)) { + this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_ + if (this->start_delay_is_valve_delay_) { + this->pump_on_(); + } else if (!this->pump_switch()->state()) { // if the pump is already on, wait to switch on the valve + this->valve_on_(); // to ensure consistent run time + } + } else { + this->run_(); // there is no start_delay_, so just start the pump and valve + } + this->pinned_millis_ = millis(); // save the time the start request was made +} + +void SprinklerValveOperator::stop() { + if ((this->state_ == IDLE) || (this->state_ == STOPPING)) { // can't stop if already stopped or stopping + return; + } + if (this->stop_delay_ && (this->pump_switch() != nullptr)) { + this->state_ = STOPPING; // STOPPING state requires both a pump and a stop_delay_ + if (this->stop_delay_is_valve_delay_) { + this->pump_off_(); + } else { + this->valve_off_(); + } + if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use... + this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time + } + this->pinned_millis_ = millis(); // save the time the stop request was made + } else { + this->kill_(); // there is no stop_delay_, so just stop the pump and valve + } +} + +uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_; } + +uint32_t SprinklerValveOperator::time_remaining() { + if ((this->state_ == STARTING) || (this->state_ == ACTIVE)) { + return (this->pinned_millis_ + this->start_delay_ + this->run_duration_ - millis()) / 1000; + } + return 0; +} + +SprinklerState SprinklerValveOperator::state() { return this->state_; } + +SprinklerSwitch *SprinklerValveOperator::pump_switch() { + if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) { + return nullptr; + } + if (this->valve_->pump_switch_index.has_value()) { + return this->controller_->valve_pump_switch_by_pump_index(this->valve_->pump_switch_index.value()); + } + return nullptr; +} + +void SprinklerValveOperator::pump_off_() { + if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + return; + } + if (this->controller_ == nullptr) { // safety first! + this->pump_switch()->turn_off(); // if no controller was set, just switch off the pump + } else { // ...otherwise, do it "safely" + auto state = this->state_; // this is silly, but... + this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does + this->controller_->set_pump_state(this->pump_switch(), false); + this->state_ = state; + } +} + +void SprinklerValveOperator::pump_on_() { + if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + return; + } + if (this->controller_ == nullptr) { // safety first! + this->pump_switch()->turn_on(); // if no controller was set, just switch on the pump + } else { // ...otherwise, do it "safely" + auto state = this->state_; // this is silly, but... + this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does + this->controller_->set_pump_state(this->pump_switch(), true); + this->state_ = state; + } +} + +void SprinklerValveOperator::valve_off_() { + if (this->valve_ == nullptr) { // safety first! + return; + } + if (this->valve_->valve_switch.state()) { + this->valve_->valve_switch.turn_off(); + } +} + +void SprinklerValveOperator::valve_on_() { + if (this->valve_ == nullptr) { // safety first! + return; + } + if (!this->valve_->valve_switch.state()) { + this->valve_->valve_switch.turn_on(); + } +} + +void SprinklerValveOperator::kill_() { + this->state_ = IDLE; + this->valve_off_(); + this->pump_off_(); +} + +void SprinklerValveOperator::run_() { + this->state_ = ACTIVE; + this->valve_on_(); + this->pump_on_(); +} + +SprinklerValveRunRequest::SprinklerValveRunRequest() {} +SprinklerValveRunRequest::SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, + SprinklerValveOperator *valve_op) + : valve_number_(valve_number), run_duration_(run_duration), valve_op_(valve_op) {} + +bool SprinklerValveRunRequest::has_request() { return this->has_valve_; } +bool SprinklerValveRunRequest::has_valve_operator() { return !(this->valve_op_ == nullptr); } + +void SprinklerValveRunRequest::set_run_duration(uint32_t run_duration) { this->run_duration_ = run_duration; } + +void SprinklerValveRunRequest::set_valve(size_t valve_number) { + this->valve_number_ = valve_number; + this->run_duration_ = 0; + this->valve_op_ = nullptr; + this->has_valve_ = true; +} + +void SprinklerValveRunRequest::set_valve_operator(SprinklerValveOperator *valve_op) { + if (valve_op != nullptr) { + this->valve_op_ = valve_op; + } +} + +void SprinklerValveRunRequest::reset() { + this->has_valve_ = false; + this->run_duration_ = 0; + this->valve_op_ = nullptr; +} + +uint32_t SprinklerValveRunRequest::run_duration() { return this->run_duration_; } + +size_t SprinklerValveRunRequest::valve() { return this->valve_number_; } + +optional SprinklerValveRunRequest::valve_as_opt() { + if (this->has_valve_) { + return this->valve_number_; + } + return nullopt; +} + +SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this->valve_op_; } + +Sprinkler::Sprinkler() {} +Sprinkler::Sprinkler(const std::string &name) : EntityBase(name) {} + +void Sprinkler::setup() { this->all_valves_off_(true); } + +void Sprinkler::loop() { + for (auto &p : this->pump_) { + p.loop(); + } + for (auto &v : this->valve_) { + v.valve_switch.loop(); + } + for (auto &vo : this->valve_op_) { + vo.loop(); + } +} + +void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) { + auto new_valve_number = this->number_of_valves(); + this->valve_.resize(new_valve_number + 1); + SprinklerValve *new_valve = &this->valve_[new_valve_number]; + + new_valve->controller_switch = valve_sw; + new_valve->controller_switch->set_state_lambda([=]() -> optional { + if (this->valve_pump_switch(new_valve_number) != nullptr) { + return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state(); + } + return this->valve_switch(new_valve_number)->state(); + }); + + new_valve->valve_turn_off_automation = + make_unique>(new_valve->controller_switch->get_turn_off_trigger()); + new_valve->valve_shutdown_action = make_unique>(this); + new_valve->valve_turn_off_automation->add_actions({new_valve->valve_shutdown_action.get()}); + + new_valve->valve_turn_on_automation = make_unique>(new_valve->controller_switch->get_turn_on_trigger()); + new_valve->valve_resumeorstart_action = make_unique>(this); + new_valve->valve_resumeorstart_action->set_valve_to_start(new_valve_number); + new_valve->valve_turn_on_automation->add_actions({new_valve->valve_resumeorstart_action.get()}); + + if (enable_sw != nullptr) { + new_valve->enable_switch = enable_sw; + new_valve->enable_switch->set_optimistic(true); + new_valve->enable_switch->set_restore_state(true); + } +} + +void Sprinkler::add_controller(Sprinkler *other_controller) { this->other_controllers_.push_back(other_controller); } + +void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller_switch) { + this->controller_sw_ = controller_switch; + controller_switch->set_state_lambda([=]() -> optional { + for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { + if (this->valve_[valve_number].controller_switch->state) { + return true; + } + } + return this->active_req_.has_request(); + }); + + this->sprinkler_turn_off_automation_ = make_unique>(controller_switch->get_turn_off_trigger()); + this->sprinkler_shutdown_action_ = make_unique>(this); + this->sprinkler_turn_off_automation_->add_actions({sprinkler_shutdown_action_.get()}); + + this->sprinkler_turn_on_automation_ = make_unique>(controller_switch->get_turn_on_trigger()); + this->sprinkler_resumeorstart_action_ = make_unique>(this); + this->sprinkler_turn_on_automation_->add_actions({sprinkler_resumeorstart_action_.get()}); +} + +void Sprinkler::set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch) { + this->auto_adv_sw_ = auto_adv_switch; + auto_adv_switch->set_optimistic(true); + auto_adv_switch->set_restore_state(true); +} + +void Sprinkler::set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch) { + this->queue_enable_sw_ = queue_enable_switch; + queue_enable_switch->set_optimistic(true); + queue_enable_switch->set_restore_state(true); +} + +void Sprinkler::set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch) { + this->reverse_sw_ = reverse_switch; + reverse_switch->set_optimistic(true); + reverse_switch->set_restore_state(true); +} + +void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { + if (this->is_a_valid_valve(valve_number)) { + this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); + this->valve_[valve_number].run_duration = run_duration; + } +} + +void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, + switch_::Switch *valve_switch_on, uint32_t pulse_duration, + uint32_t run_duration) { + if (this->is_a_valid_valve(valve_number)) { + this->valve_[valve_number].valve_switch.set_off_switch(valve_switch_off); + this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); + this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); + this->valve_[valve_number].run_duration = run_duration; + } +} + +void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) { + if (this->is_a_valid_valve(valve_number)) { + for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump + if (this->pump_[i].on_switch() == pump_switch) { // if the "new" pump matches one we already have... + this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... + return; // ...and we are done + } + } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it + this->pump_.resize(this->pump_.size() + 1); + this->pump_.back().set_on_switch(pump_switch); + this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump + } +} + +void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, + switch_::Switch *pump_switch_on, uint32_t pulse_duration) { + if (this->is_a_valid_valve(valve_number)) { + for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump + if ((this->pump_[i].off_switch() == pump_switch_off) && + (this->pump_[i].on_switch() == pump_switch_on)) { // if the "new" pump matches one we already have... + this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... + return; // ...and we are done + } + } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it + this->pump_.resize(this->pump_.size() + 1); + this->pump_.back().set_off_switch(pump_switch_off); + this->pump_.back().set_on_switch(pump_switch_on); + this->pump_.back().set_pulse_duration(pulse_duration); + this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump + } +} + +void Sprinkler::set_multiplier(const optional multiplier) { + if (multiplier.has_value()) { + if (multiplier.value() > 0) { + this->multiplier_ = multiplier.value(); + } + } +} + +void Sprinkler::set_pump_start_delay(uint32_t start_delay) { + this->start_delay_is_valve_delay_ = false; + this->start_delay_ = start_delay; +} + +void Sprinkler::set_pump_stop_delay(uint32_t stop_delay) { + this->stop_delay_is_valve_delay_ = false; + this->stop_delay_ = stop_delay; +} + +void Sprinkler::set_valve_start_delay(uint32_t start_delay) { + this->start_delay_is_valve_delay_ = true; + this->start_delay_ = start_delay; +} + +void Sprinkler::set_valve_stop_delay(uint32_t stop_delay) { + this->stop_delay_is_valve_delay_ = true; + this->stop_delay_ = stop_delay; +} + +void Sprinkler::set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay) { + this->pump_switch_off_during_valve_open_delay_ = pump_switch_off_during_valve_open_delay; +} + +void Sprinkler::set_valve_open_delay(const uint32_t valve_open_delay) { + if (valve_open_delay > 0) { + this->valve_overlap_ = false; + this->switching_delay_ = valve_open_delay; + } else { + this->switching_delay_.reset(); + } +} + +void Sprinkler::set_valve_overlap(uint32_t valve_overlap) { + if (valve_overlap > 0) { + this->valve_overlap_ = true; + this->switching_delay_ = valve_overlap; + } else { + this->switching_delay_.reset(); + } + this->pump_switch_off_during_valve_open_delay_ = false; // incompatible option +} + +void Sprinkler::set_manual_selection_delay(uint32_t manual_selection_delay) { + if (manual_selection_delay > 0) { + this->manual_selection_delay_ = manual_selection_delay; + } else { + this->manual_selection_delay_.reset(); + } +} + +void Sprinkler::set_valve_run_duration(const optional valve_number, const optional run_duration) { + if (valve_number.has_value() && run_duration.has_value()) { + if (this->is_a_valid_valve(valve_number.value())) { + this->valve_[valve_number.value()].run_duration = run_duration.value(); + } + } +} + +void Sprinkler::set_auto_advance(const bool auto_advance) { + if (this->auto_adv_sw_ != nullptr) { + this->auto_adv_sw_->publish_state(auto_advance); + } +} + +void Sprinkler::set_repeat(optional repeat) { this->target_repeats_ = repeat; } + +void Sprinkler::set_queue_enable(bool queue_enable) { + if (this->queue_enable_sw_ != nullptr) { + this->queue_enable_sw_->publish_state(queue_enable); + } +} + +void Sprinkler::set_reverse(const bool reverse) { + if (this->reverse_sw_ != nullptr) { + this->reverse_sw_->publish_state(reverse); + } +} + +uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].run_duration; + } + return 0; +} + +uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) { + uint32_t run_duration = 0; + + if (this->is_a_valid_valve(valve_number)) { + run_duration = this->valve_[valve_number].run_duration; + } + run_duration = static_cast(roundf(run_duration * this->multiplier_)); + // run_duration must not be less than any of these + if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) || + (run_duration < this->switching_delay_.value_or(0) * 2)) { + return std::max(this->switching_delay_.value_or(0) * 2, std::max(this->start_delay_, this->stop_delay_)); + } + return run_duration; +} + +bool Sprinkler::auto_advance() { + if (this->auto_adv_sw_ != nullptr) { + return this->auto_adv_sw_->state; + } + return false; +} + +float Sprinkler::multiplier() { return this->multiplier_; } + +optional Sprinkler::repeat() { return this->target_repeats_; } + +optional Sprinkler::repeat_count() { + // if there is an active valve and auto-advance is enabled, we may be repeating, so return the count + if (this->auto_adv_sw_ != nullptr) { + if (this->active_req_.has_request() && this->auto_adv_sw_->state) { + return this->repeat_count_; + } + } + return nullopt; +} + +bool Sprinkler::queue_enabled() { + if (this->queue_enable_sw_ != nullptr) { + return this->queue_enable_sw_->state; + } + return true; +} + +bool Sprinkler::reverse() { + if (this->reverse_sw_ != nullptr) { + return this->reverse_sw_->state; + } + return false; +} + +void Sprinkler::start_from_queue() { + if (this->queued_valves_.empty()) { + return; // if there is nothing in the queue, don't do anything + } + if (this->queue_enabled() && this->active_valve().has_value()) { + return; // if there is already a valve running from the queue, do nothing + } + + if (this->auto_adv_sw_ != nullptr) { + this->auto_adv_sw_->publish_state(false); + } + if (this->queue_enable_sw_ != nullptr) { + this->queue_enable_sw_->publish_state(true); + } + this->reset_cycle_states_(); // just in case auto-advance is switched on later + this->repeat_count_ = 0; + this->fsm_kick_(); // will automagically pick up from the queue (it has priority) +} + +void Sprinkler::start_full_cycle() { + if (this->auto_advance() && this->active_valve().has_value()) { + return; // if auto-advance is already enabled and there is already a valve running, do nothing + } + + if (this->queue_enable_sw_ != nullptr) { + this->queue_enable_sw_->publish_state(false); + } + this->prep_full_cycle_(); + this->repeat_count_ = 0; + // if there is no active valve already, start the first valve in the cycle + if (!this->active_req_.has_request()) { + this->fsm_kick_(); + } +} + +void Sprinkler::start_single_valve(const optional valve_number) { + if (!valve_number.has_value() || (valve_number == this->active_valve())) { + return; + } + + if (this->auto_adv_sw_ != nullptr) { + this->auto_adv_sw_->publish_state(false); + } + if (this->queue_enable_sw_ != nullptr) { + this->queue_enable_sw_->publish_state(false); + } + this->reset_cycle_states_(); // just in case auto-advance is switched on later + this->repeat_count_ = 0; + this->fsm_request_(valve_number.value()); +} + +void Sprinkler::queue_valve(optional valve_number, optional run_duration) { + if (valve_number.has_value()) { + if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { + SprinklerQueueItem item{valve_number.value(), run_duration.value()}; + this->queued_valves_.insert(this->queued_valves_.begin(), item); + ESP_LOGD(TAG, "Valve %u placed into queue with run duration of %u seconds", valve_number.value_or(0), + run_duration.value_or(0)); + } + } +} + +void Sprinkler::clear_queued_valves() { + this->queued_valves_.clear(); + ESP_LOGD(TAG, "Queue cleared"); +} + +void Sprinkler::next_valve() { + if (this->state_ == IDLE) { + this->reset_cycle_states_(); // just in case auto-advance is switched on later + } + this->manual_valve_ = this->next_valve_number_( + this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(this->number_of_valves() - 1))); + if (this->manual_selection_delay_.has_value()) { + this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); + this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); + } else { + this->fsm_request_(this->manual_valve_.value()); + } +} + +void Sprinkler::previous_valve() { + if (this->state_ == IDLE) { + this->reset_cycle_states_(); // just in case auto-advance is switched on later + } + this->manual_valve_ = + this->previous_valve_number_(this->manual_valve_.value_or(this->active_req_.valve_as_opt().value_or(0))); + if (this->manual_selection_delay_.has_value()) { + this->set_timer_duration_(sprinkler::TIMER_VALVE_SELECTION, this->manual_selection_delay_.value()); + this->start_timer_(sprinkler::TIMER_VALVE_SELECTION); + } else { + this->fsm_request_(this->manual_valve_.value()); + } +} + +void Sprinkler::shutdown(bool clear_queue) { + this->cancel_timer_(sprinkler::TIMER_VALVE_SELECTION); + this->active_req_.reset(); + this->manual_valve_.reset(); + this->next_req_.reset(); + for (auto &vo : this->valve_op_) { + vo.stop(); + } + this->fsm_transition_to_shutdown_(); + if (clear_queue) { + this->clear_queued_valves(); + this->repeat_count_ = 0; + } +} + +void Sprinkler::pause() { + if (this->paused_valve_.has_value() || !this->active_req_.has_request()) { + return; // we can't pause if we're already paused or if there is no active valve + } + this->paused_valve_ = this->active_valve(); + this->resume_duration_ = this->time_remaining(); + this->shutdown(false); + ESP_LOGD(TAG, "Paused valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); +} + +void Sprinkler::resume() { + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { + ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); + this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + this->reset_resume_(); + } else { + ESP_LOGD(TAG, "No valve to resume!"); + } +} + +void Sprinkler::resume_or_start_full_cycle() { + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { + this->resume(); + } else { + this->start_full_cycle(); + } +} + +const char *Sprinkler::valve_name(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].controller_switch->get_name().c_str(); + } + return nullptr; +} + +optional Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } +optional Sprinkler::paused_valve() { return this->paused_valve_; } + +optional Sprinkler::queued_valve() { + if (!this->queued_valves_.empty()) { + return this->queued_valves_.back().valve_number; + } + return nullopt; +} + +optional Sprinkler::manual_valve() { return this->manual_valve_; } + +size_t Sprinkler::number_of_valves() { return this->valve_.size(); } + +bool Sprinkler::is_a_valid_valve(const size_t valve_number) { + return ((valve_number >= 0) && (valve_number < this->number_of_valves())); +} + +bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { + if (pump_switch == nullptr) { + return false; // we can't do anything if there's nothing to check + } + // a pump must be considered "in use" if a (distribution) valve it supplies is active. this means: + // - at least one SprinklerValveOperator: + // - has a valve loaded that depends on this pump + // - is in a state that depends on the pump: (ACTIVE and _possibly_ STARTING/STOPPING) + // - if NO SprinklerValveOperator is active but there is a run request pending (active_req_.has_request()) and the + // controller state is STARTING, valve open delay is configured but NOT pump_switch_off_during_valve_open_delay_ + for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump + if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) { + // the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest + if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && + (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { + // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or + // is + // STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now + if ((vo.state() == ACTIVE) || + ((vo.state() == STARTING) && this->start_delay_ && this->start_delay_is_valve_delay_) || + ((vo.state() == STOPPING) && this->stop_delay_ && this->stop_delay_is_valve_delay_)) { + return true; + } + } + } + } // if we end up here, no SprinklerValveOperator was in a "give-away" state indicating that the pump is in use... + if (!this->valve_overlap_ && !this->pump_switch_off_during_valve_open_delay_ && this->switching_delay_.has_value() && + this->active_req_.has_request() && (this->state_ != STOPPING)) { + // ...the controller is configured to keep the pump on during a valve open delay, so just return + // whether or not the next valve shares the same pump + return (pump_switch->off_switch() == this->valve_pump_switch(this->active_req_.valve())->off_switch()) && + (pump_switch->on_switch() == this->valve_pump_switch(this->active_req_.valve())->on_switch()); + } + return false; +} + +void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { + if (pump_switch == nullptr) { + return; // we can't do anything if there's nothing to check + } + + bool hold_pump_on = false; + + for (auto &controller : this->other_controllers_) { // check if the pump is in use by another controller + if (controller != this) { // dummy check + if (controller->pump_in_use(pump_switch)) { + hold_pump_on = true; // if another controller says it's using this pump, keep it on + // at this point we know if there exists another SprinklerSwitch that is "on" with its + // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects + } + } + } + if (hold_pump_on) { + // at this point we know if there exists another SprinklerSwitch that is "on" with its + // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects... + pump_switch->sync_valve_state(true); // ...so ensure our state is consistent + ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it"); + } + + if (state) { // ...and now we can set the new state of the switch + pump_switch->turn_on(); + } else if (!hold_pump_on && !this->pump_in_use(pump_switch)) { + pump_switch->turn_off(); + } else if (hold_pump_on) { // we must assume the other controller will switch off the pump when done... + pump_switch->sync_valve_state(false); // ...this only impacts latching valves + } +} + +optional Sprinkler::time_remaining() { + if (this->active_req_.has_request()) { // first try to return the value based on active_req_... + if (this->active_req_.valve_operator() != nullptr) { + return this->active_req_.valve_operator()->time_remaining(); + } + } + for (auto &vo : this->valve_op_) { // ...else return the value from the first non-IDLE SprinklerValveOperator + if (vo.state() != IDLE) { + return vo.time_remaining(); + } + } + return nullopt; +} + +SprinklerControllerSwitch *Sprinkler::control_switch(size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].controller_switch; + } + return nullptr; +} + +SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].enable_switch; + } + return nullptr; +} + +SprinklerSwitch *Sprinkler::valve_switch(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return &this->valve_[valve_number].valve_switch; + } + return nullptr; +} + +SprinklerSwitch *Sprinkler::valve_pump_switch(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { + return &this->pump_[this->valve_[valve_number].pump_switch_index.value()]; + } + return nullptr; +} + +SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { + if (pump_index < this->pump_.size()) { + return &this->pump_[pump_index]; + } + return nullptr; +} + +uint32_t Sprinkler::hash_base() { return 3129891955UL; } + +bool Sprinkler::valve_is_enabled_(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + if (this->valve_[valve_number].enable_switch != nullptr) { + return this->valve_[valve_number].enable_switch->state; + } else { + return true; + } + } + return false; +} + +void Sprinkler::mark_valve_cycle_complete_(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + ESP_LOGD(TAG, "Marking valve %u complete", valve_number); + this->valve_[valve_number].valve_cycle_complete = true; + } +} + +bool Sprinkler::valve_cycle_complete_(const size_t valve_number) { + if (this->is_a_valid_valve(valve_number)) { + return this->valve_[valve_number].valve_cycle_complete; + } + return false; +} + +size_t Sprinkler::next_valve_number_(const size_t first_valve) { + if (this->is_a_valid_valve(first_valve) && (first_valve + 1 < this->number_of_valves())) + return first_valve + 1; + + return 0; +} + +size_t Sprinkler::previous_valve_number_(const size_t first_valve) { + if (this->is_a_valid_valve(first_valve) && (first_valve - 1 >= 0)) + return first_valve - 1; + + return this->number_of_valves() - 1; +} + +optional Sprinkler::next_valve_number_in_cycle_(const optional first_valve) { + if (this->reverse_sw_ != nullptr) { + if (this->reverse_sw_->state) { + return this->previous_enabled_incomplete_valve_number_(first_valve); + } + } + return this->next_enabled_incomplete_valve_number_(first_valve); +} + +void Sprinkler::load_next_valve_run_request_(optional first_valve) { + if (this->next_req_.has_request()) { + if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on + this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve())); + } + return; // there is already a request pending + } else if (this->queue_enabled() && !this->queued_valves_.empty()) { + this->next_req_.set_valve(this->queued_valves_.back().valve_number); + if (this->queued_valves_.back().run_duration) { + this->next_req_.set_run_duration(this->queued_valves_.back().run_duration); + } else { + this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->queued_valves_.back().valve_number)); + } + this->queued_valves_.pop_back(); + } else if (this->auto_adv_sw_ != nullptr) { + if (this->auto_adv_sw_->state) { + if (this->next_valve_number_in_cycle_(first_valve).has_value()) { + // if there is another valve to run as a part of a cycle, load that + this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + this->next_req_.set_run_duration( + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + } else if ((this->repeat_count_++ < this->target_repeats_.value_or(0))) { + ESP_LOGD(TAG, "Repeating - starting cycle %u of %u", this->repeat_count_ + 1, + this->target_repeats_.value_or(0) + 1); + // if there are repeats remaining and no more valves were left in the cycle, start a new cycle + this->prep_full_cycle_(); + this->next_req_.set_valve(this->next_valve_number_in_cycle_(first_valve).value_or(0)); + this->next_req_.set_run_duration( + this->valve_run_duration_adjusted(this->next_valve_number_in_cycle_(first_valve).value_or(0))); + } + } + } +} + +optional Sprinkler::next_enabled_incomplete_valve_number_(const optional first_valve) { + auto new_valve_number = this->next_valve_number_(first_valve.value_or(this->number_of_valves() - 1)); + + while (new_valve_number != first_valve.value_or(this->number_of_valves() - 1)) { + if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { + return new_valve_number; + } else { + new_valve_number = this->next_valve_number_(new_valve_number); + } + } + return nullopt; +} + +optional Sprinkler::previous_enabled_incomplete_valve_number_(const optional first_valve) { + auto new_valve_number = this->previous_valve_number_(first_valve.value_or(0)); + + while (new_valve_number != first_valve.value_or(0)) { + if (this->valve_is_enabled_(new_valve_number) && (!this->valve_cycle_complete_(new_valve_number))) { + return new_valve_number; + } else { + new_valve_number = this->previous_valve_number_(new_valve_number); + } + } + return nullopt; +} + +bool Sprinkler::any_valve_is_enabled_() { + for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { + if (this->valve_is_enabled_(valve_number)) + return true; + } + return false; +} + +void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { + if (!req->has_request()) { + return; // we can't do anything if the request contains nothing + } + if (!this->is_a_valid_valve(req->valve())) { + return; // we can't do anything if the valve number isn't valid + } + for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up + if (vo.state() == IDLE) { + auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); + ESP_LOGD(TAG, "Starting valve %u for %u seconds, cycle %u of %u", req->valve(), run_duration, + this->repeat_count_ + 1, this->target_repeats_.value_or(0) + 1); + req->set_valve_operator(&vo); + vo.set_controller(this); + vo.set_valve(&this->valve_[req->valve()]); + vo.set_run_duration(run_duration); + vo.set_start_delay(this->start_delay_, this->start_delay_is_valve_delay_); + vo.set_stop_delay(this->stop_delay_, this->stop_delay_is_valve_delay_); + vo.start(); + return; + } + } +} + +void Sprinkler::all_valves_off_(const bool include_pump) { + for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) { + if (this->valve_[valve_index].valve_switch.state()) { + this->valve_[valve_index].valve_switch.turn_off(); + } + if (include_pump) { + this->set_pump_state(this->valve_pump_switch(valve_index), false); + } + } + ESP_LOGD(TAG, "All valves stopped%s", include_pump ? ", including pumps" : ""); +} + +void Sprinkler::prep_full_cycle_() { + if (this->auto_adv_sw_ != nullptr) { + if (!this->auto_adv_sw_->state) { + this->auto_adv_sw_->publish_state(true); + } + } + if (!this->any_valve_is_enabled_()) { + for (auto &valve : this->valve_) { + if (valve.enable_switch != nullptr) { + valve.enable_switch->publish_state(true); + } + } + } + this->reset_cycle_states_(); +} + +void Sprinkler::reset_cycle_states_() { + for (auto &valve : this->valve_) { + valve.valve_cycle_complete = false; + } +} + +void Sprinkler::reset_resume_() { + this->paused_valve_.reset(); + this->resume_duration_.reset(); +} + +void Sprinkler::fsm_request_(size_t requested_valve, uint32_t requested_run_duration) { + this->next_req_.set_valve(requested_valve); + this->next_req_.set_run_duration(requested_run_duration); + // if state is IDLE or ACTIVE, call fsm_transition_() to start it immediately; + // otherwise, fsm_transition() will pick up next_req_ at the next appropriate transition + this->fsm_kick_(); +} + +void Sprinkler::fsm_kick_() { + if ((this->state_ == IDLE) || (this->state_ == ACTIVE)) { + this->fsm_transition_(); + } +} + +void Sprinkler::fsm_transition_() { + ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", this->state_as_str_(this->state_).c_str()); + switch (this->state_) { + case IDLE: // the system was off -> start it up + // advances to ACTIVE + this->fsm_transition_from_shutdown_(); + break; + + case ACTIVE: + // advances to STOPPING or ACTIVE (again) + this->fsm_transition_from_valve_run_(); + break; + + case STARTING: { + // follows valve open delay interval + this->set_timer_duration_(sprinkler::TIMER_SM, + this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + this->start_timer_(sprinkler::TIMER_SM); + this->start_valve_(&this->active_req_); + this->state_ = ACTIVE; + if (this->next_req_.has_request()) { + // another valve has been requested, so restart the timer so we pick it up quickly + this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); + this->start_timer_(sprinkler::TIMER_SM); + } + break; + } + + case STOPPING: + // stop_delay_ has elapsed so just shut everything off + this->active_req_.reset(); + this->manual_valve_.reset(); + this->all_valves_off_(true); + this->state_ = IDLE; + break; + + default: + break; + } + if (this->next_req_.has_request() && (this->state_ == IDLE)) { + // another valve has been requested, so restart the timer so we pick it up quickly + this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); + this->start_timer_(sprinkler::TIMER_SM); + } + ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", this->state_as_str_(this->state_).c_str()); +} + +void Sprinkler::fsm_transition_from_shutdown_() { + this->load_next_valve_run_request_(); + this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_run_duration(this->next_req_.run_duration()); + this->next_req_.reset(); + + this->set_timer_duration_(sprinkler::TIMER_SM, this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + this->start_timer_(sprinkler::TIMER_SM); + this->start_valve_(&this->active_req_); + this->state_ = ACTIVE; +} + +void Sprinkler::fsm_transition_from_valve_run_() { + if (!this->active_req_.has_request()) { // dummy check... + this->fsm_transition_to_shutdown_(); + return; + } + + if (!this->timer_active_(sprinkler::TIMER_SM)) { // only flag the valve as "complete" if the timer finished + this->mark_valve_cycle_complete_(this->active_req_.valve()); + } else { + ESP_LOGD(TAG, "Valve cycle interrupted - NOT flagging valve as complete and stopping current valve"); + for (auto &vo : this->valve_op_) { + vo.stop(); + } + } + + this->load_next_valve_run_request_(this->active_req_.valve()); + + if (this->next_req_.has_request()) { // there is another valve to run... + bool same_pump = + this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve()); + + this->active_req_.set_valve(this->next_req_.valve()); + this->active_req_.set_run_duration(this->next_req_.run_duration()); + this->next_req_.reset(); + + // this->state_ = ACTIVE; // state isn't changing + if (this->valve_overlap_ || !this->switching_delay_.has_value()) { + this->set_timer_duration_(sprinkler::TIMER_SM, + this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + this->start_timer_(sprinkler::TIMER_SM); + this->start_valve_(&this->active_req_); + } else { + this->set_timer_duration_( + sprinkler::TIMER_SM, + this->switching_delay_.value() * 2 + + (this->pump_switch_off_during_valve_open_delay_ && same_pump ? this->stop_delay_ : 0)); + this->start_timer_(sprinkler::TIMER_SM); + this->state_ = STARTING; + } + } else { // there is NOT another valve to run... + this->fsm_transition_to_shutdown_(); + } +} + +void Sprinkler::fsm_transition_to_shutdown_() { + this->state_ = STOPPING; + this->set_timer_duration_(sprinkler::TIMER_SM, + this->start_delay_ + this->stop_delay_ + this->switching_delay_.value_or(0) + 1); + this->start_timer_(sprinkler::TIMER_SM); +} + +std::string Sprinkler::state_as_str_(SprinklerState state) { + switch (state) { + case IDLE: + return "IDLE"; + + case STARTING: + return "STARTING"; + + case ACTIVE: + return "ACTIVE"; + + case STOPPING: + return "STOPPING"; + + case BYPASS: + return "BYPASS"; + + default: + return "UNKNOWN"; + } +} + +void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { + if (this->timer_duration_(timer_index) > 0) { + this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + this->timer_cbf_(timer_index)); + this->timer_[timer_index].start_time = millis(); + this->timer_[timer_index].active = true; + } + ESP_LOGVV(TAG, "Timer %u started for %u sec", static_cast(timer_index), + this->timer_duration_(timer_index) / 1000); +} + +bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { + this->timer_[timer_index].active = false; + return this->cancel_timeout(this->timer_[timer_index].name); +} + +bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } + +void Sprinkler::set_timer_duration_(const SprinklerTimerIndex timer_index, const uint32_t time) { + this->timer_[timer_index].time = 1000 * time; +} + +uint32_t Sprinkler::timer_duration_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].time; } + +std::function Sprinkler::timer_cbf_(const SprinklerTimerIndex timer_index) { + return this->timer_[timer_index].func; +} + +void Sprinkler::valve_selection_callback_() { + this->timer_[sprinkler::TIMER_VALVE_SELECTION].active = false; + ESP_LOGVV(TAG, "Valve selection timer expired"); + if (this->manual_valve_.has_value()) { + this->fsm_request_(this->manual_valve_.value()); + this->manual_valve_.reset(); + } +} + +void Sprinkler::sm_timer_callback_() { + this->timer_[sprinkler::TIMER_SM].active = false; + ESP_LOGVV(TAG, "State machine timer expired"); + this->fsm_transition_(); +} + +void Sprinkler::dump_config() { + ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_.c_str()); + if (this->manual_selection_delay_.has_value()) { + ESP_LOGCONFIG(TAG, " Manual Selection Delay: %u seconds", this->manual_selection_delay_.value_or(0)); + } + if (this->target_repeats_.has_value()) { + ESP_LOGCONFIG(TAG, " Repeat Cycles: %u times", this->target_repeats_.value_or(0)); + } + if (this->start_delay_) { + if (this->start_delay_is_valve_delay_) { + ESP_LOGCONFIG(TAG, " Pump Start Valve Delay: %u seconds", this->start_delay_); + } else { + ESP_LOGCONFIG(TAG, " Pump Start Pump Delay: %u seconds", this->start_delay_); + } + } + if (this->stop_delay_) { + if (this->stop_delay_is_valve_delay_) { + ESP_LOGCONFIG(TAG, " Pump Stop Valve Delay: %u seconds", this->stop_delay_); + } else { + ESP_LOGCONFIG(TAG, " Pump Stop Pump Delay: %u seconds", this->stop_delay_); + } + } + if (this->switching_delay_.has_value()) { + if (this->valve_overlap_) { + ESP_LOGCONFIG(TAG, " Valve Overlap: %u seconds", this->switching_delay_.value_or(0)); + } else { + ESP_LOGCONFIG(TAG, " Valve Open Delay: %u seconds", this->switching_delay_.value_or(0)); + ESP_LOGCONFIG(TAG, " Pump Switch Off During Valve Open Delay: %s", + YESNO(this->pump_switch_off_during_valve_open_delay_)); + } + } + for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) { + ESP_LOGCONFIG(TAG, " Valve %u:", valve_number); + ESP_LOGCONFIG(TAG, " Name: %s", this->valve_name(valve_number)); + ESP_LOGCONFIG(TAG, " Run Duration: %u seconds", this->valve_[valve_number].run_duration); + if (this->valve_[valve_number].valve_switch.pulse_duration()) { + ESP_LOGCONFIG(TAG, " Pulse Duration: %u milliseconds", + this->valve_[valve_number].valve_switch.pulse_duration()); + } + } + if (!this->pump_.empty()) { + ESP_LOGCONFIG(TAG, " Total number of pumps: %u", this->pump_.size()); + } + if (!this->valve_.empty()) { + ESP_LOGCONFIG(TAG, " Total number of valves: %u", this->valve_.size()); + } +} + +} // namespace sprinkler +} // namespace esphome diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h new file mode 100644 index 0000000000..1243a844fa --- /dev/null +++ b/esphome/components/sprinkler/sprinkler.h @@ -0,0 +1,528 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace sprinkler { + +enum SprinklerState : uint8_t { + // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! + IDLE, // system/valve is off + STARTING, // system/valve is starting/"half open" -- either pump or valve is on, but the remaining pump/valve is not + ACTIVE, // system/valve is running its cycle + STOPPING, // system/valve is stopping/"half open" -- either pump or valve is on, but the remaining pump/valve is not + BYPASS // used by SprinklerValveOperator to ignore the instance checking pump status +}; + +enum SprinklerTimerIndex : uint8_t { + TIMER_SM = 0, + TIMER_VALVE_SELECTION = 1, +}; + +class Sprinkler; // this component +class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core +class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves +class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps +class SprinklerValveRunRequest; // tells the sprinkler controller what valve to run and for how long as well as what + // SprinklerValveOperator is handling it +template class StartSingleValveAction; +template class ShutdownAction; +template class ResumeOrStartAction; + +class SprinklerSwitch { + public: + SprinklerSwitch(); + SprinklerSwitch(switch_::Switch *sprinkler_switch); + SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration); + + bool is_latching_valve(); // returns true if configured as a latching valve + void loop(); // called as a part of loop(), used for latching valve pulses + uint32_t pulse_duration() { return this->pulse_duration_; } + bool state(); // returns the switch's current state + void set_off_switch(switch_::Switch *off_switch) { this->off_switch_ = off_switch; } + void set_on_switch(switch_::Switch *on_switch) { this->on_switch_ = on_switch; } + void set_pulse_duration(uint32_t pulse_duration) { this->pulse_duration_ = pulse_duration; } + void sync_valve_state( + bool latch_state); // syncs internal state to switch; if latching valve, sets state to latch_state + void turn_off(); // sets internal flag and actuates the switch + void turn_on(); // sets internal flag and actuates the switch + switch_::Switch *off_switch() { return this->off_switch_; } + switch_::Switch *on_switch() { return this->on_switch_; } + + protected: + bool state_{false}; + uint32_t pulse_duration_{0}; + uint64_t pinned_millis_{0}; + switch_::Switch *off_switch_{nullptr}; // only used for latching valves + switch_::Switch *on_switch_{nullptr}; // used for both latching and non-latching valves +}; + +struct SprinklerQueueItem { + size_t valve_number; + uint32_t run_duration; +}; + +struct SprinklerTimer { + const std::string name; + bool active; + uint32_t time; + uint32_t start_time; + std::function func; +}; + +struct SprinklerValve { + SprinklerControllerSwitch *controller_switch; + SprinklerControllerSwitch *enable_switch; + SprinklerSwitch valve_switch; + uint32_t run_duration; + optional pump_switch_index; + bool valve_cycle_complete; + std::unique_ptr> valve_shutdown_action; + std::unique_ptr> valve_resumeorstart_action; + std::unique_ptr> valve_turn_off_automation; + std::unique_ptr> valve_turn_on_automation; +}; + +class SprinklerControllerSwitch : public switch_::Switch, public Component { + public: + SprinklerControllerSwitch(); + + void setup() override; + void dump_config() override; + + void set_state_lambda(std::function()> &&f); + void set_restore_state(bool restore_state); + Trigger<> *get_turn_on_trigger() const; + Trigger<> *get_turn_off_trigger() const; + void set_optimistic(bool optimistic); + void set_assumed_state(bool assumed_state); + void loop() override; + + float get_setup_priority() const override; + + protected: + bool assumed_state() override; + + void write_state(bool state) override; + + optional()>> f_; + bool optimistic_{false}; + bool assumed_state_{false}; + Trigger<> *turn_on_trigger_; + Trigger<> *turn_off_trigger_; + Trigger<> *prev_trigger_{nullptr}; + bool restore_state_{false}; +}; + +class SprinklerValveOperator { + public: + SprinklerValveOperator(); + SprinklerValveOperator(SprinklerValve *valve, Sprinkler *controller); + void loop(); + void set_controller(Sprinkler *controller); + void set_valve(SprinklerValve *valve); + void set_run_duration(uint32_t run_duration); // set the desired run duration in seconds + void set_start_delay(uint32_t start_delay, bool start_delay_is_valve_delay); + void set_stop_delay(uint32_t stop_delay, bool stop_delay_is_valve_delay); + void start(); + void stop(); + uint32_t run_duration(); // returns the desired run duration in seconds + uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_) + SprinklerState state(); // returns the valve's state/status + SprinklerSwitch *pump_switch(); // returns this SprinklerValveOperator's pump's SprinklerSwitch + + protected: + void pump_off_(); + void pump_on_(); + void valve_off_(); + void valve_on_(); + void kill_(); + void run_(); + bool start_delay_is_valve_delay_{false}; + bool stop_delay_is_valve_delay_{false}; + uint32_t start_delay_{0}; + uint32_t stop_delay_{0}; + uint32_t run_duration_{0}; + uint64_t pinned_millis_{0}; + Sprinkler *controller_{nullptr}; + SprinklerValve *valve_{nullptr}; + SprinklerState state_{IDLE}; +}; + +class SprinklerValveRunRequest { + public: + SprinklerValveRunRequest(); + SprinklerValveRunRequest(size_t valve_number, uint32_t run_duration, SprinklerValveOperator *valve_op); + bool has_request(); + bool has_valve_operator(); + void set_run_duration(uint32_t run_duration); + void set_valve(size_t valve_number); + void set_valve_operator(SprinklerValveOperator *valve_op); + void reset(); + uint32_t run_duration(); + size_t valve(); + optional valve_as_opt(); + SprinklerValveOperator *valve_operator(); + + protected: + bool has_valve_{false}; + size_t valve_number_{0}; + uint32_t run_duration_{0}; + SprinklerValveOperator *valve_op_{nullptr}; +}; + +class Sprinkler : public Component, public EntityBase { + public: + Sprinkler(); + Sprinkler(const std::string &name); + + void setup() override; + void loop() override; + void dump_config() override; + + /// add a valve to the controller + void add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw = nullptr); + + /// add another controller to the controller so it can check if pumps/main valves are in use + void add_controller(Sprinkler *other_controller); + + /// configure important controller switches + void set_controller_main_switch(SprinklerControllerSwitch *controller_switch); + void set_controller_auto_adv_switch(SprinklerControllerSwitch *auto_adv_switch); + void set_controller_queue_enable_switch(SprinklerControllerSwitch *queue_enable_switch); + void set_controller_reverse_switch(SprinklerControllerSwitch *reverse_switch); + + /// configure a valve's switch object and run duration. run_duration is time in seconds. + void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); + void configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, + switch_::Switch *valve_switch_on, uint32_t pulse_duration, uint32_t run_duration); + + /// configure a valve's associated pump switch object + void configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch); + void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, + switch_::Switch *pump_switch_on, uint32_t pulse_duration); + + /// value multiplied by configured run times -- used to extend or shorten the cycle + void set_multiplier(optional multiplier); + + /// set how long the pump should start after the valve (when the pump is starting) + void set_pump_start_delay(uint32_t start_delay); + + /// set how long the pump should stop after the valve (when the pump is starting) + void set_pump_stop_delay(uint32_t stop_delay); + + /// set how long the valve should start after the pump (when the pump is stopping) + void set_valve_start_delay(uint32_t start_delay); + + /// set how long the valve should stop after the pump (when the pump is stopping) + void set_valve_stop_delay(uint32_t stop_delay); + + /// if pump_switch_off_during_valve_open_delay is true, the controller will switch off the pump during the + /// valve_open_delay interval + void set_pump_switch_off_during_valve_open_delay(bool pump_switch_off_during_valve_open_delay); + + /// set how long the controller should wait to open/switch on the valve after it becomes active + void set_valve_open_delay(uint32_t valve_open_delay); + + /// set how long the controller should wait after opening a valve before closing the previous valve + void set_valve_overlap(uint32_t valve_overlap); + + /// set how long the controller should wait to activate a valve after next_valve() or previous_valve() is called + void set_manual_selection_delay(uint32_t manual_selection_delay); + + /// set how long the valve should remain on/open. run_duration is time in seconds + void set_valve_run_duration(optional valve_number, optional run_duration); + + /// if auto_advance is true, controller will iterate through all enabled valves + void set_auto_advance(bool auto_advance); + + /// set the number of times to repeat a full cycle + void set_repeat(optional repeat); + + /// if queue_enable is true, controller will iterate through valves in the queue + void set_queue_enable(bool queue_enable); + + /// if reverse is true, controller will iterate through all enabled valves in reverse (descending) order + void set_reverse(bool reverse); + + /// returns valve_number's run duration in seconds + uint32_t valve_run_duration(size_t valve_number); + + /// returns valve_number's run duration (in seconds) adjusted by multiplier_ + uint32_t valve_run_duration_adjusted(size_t valve_number); + + /// returns true if auto_advance is enabled + bool auto_advance(); + + /// returns the current value of the multiplier + float multiplier(); + + /// returns the number of times the controller is set to repeat cycles, if at all. check with 'has_value()' + optional repeat(); + + /// if a cycle is active, returns the number of times the controller has repeated the cycle. check with 'has_value()' + optional repeat_count(); + + /// returns true if the queue is enabled to run + bool queue_enabled(); + + /// returns true if reverse is enabled + bool reverse(); + + /// starts the controller from the first valve in the queue and disables auto_advance. + /// if the queue is empty, does nothing. + void start_from_queue(); + + /// starts a full cycle of all enabled valves and enables auto_advance. + /// if no valves are enabled, all valves will be enabled. + void start_full_cycle(); + + /// activates a single valve and disables auto_advance. + void start_single_valve(optional valve_number); + + /// adds a valve into the queue. queued valves have priority over valves to be run as a part of a full cycle. + /// NOTE: queued valves will always run, regardless of auto-advance and/or valve enable switches. + void queue_valve(optional valve_number, optional run_duration); + + /// clears/removes all valves from the queue + void clear_queued_valves(); + + /// advances to the next valve (numerically) + void next_valve(); + + /// advances to the previous valve (numerically) + void previous_valve(); + + /// turns off all valves, effectively shutting down the system. + void shutdown(bool clear_queue = false); + + /// same as shutdown(), but also stores active_valve() and time_remaining() allowing resume() to continue the cycle + void pause(); + + /// resumes a cycle that was suspended using pause() + void resume(); + + /// if a cycle was suspended using pause(), resumes it. otherwise calls start_full_cycle() + void resume_or_start_full_cycle(); + + /// returns a pointer to a valve's name string object; returns nullptr if valve_number is invalid + const char *valve_name(size_t valve_number); + + /// returns the number of the valve that is currently active, if any. check with 'has_value()' + optional active_valve(); + + /// returns the number of the valve that is paused, if any. check with 'has_value()' + optional paused_valve(); + + /// returns the number of the next valve in the queue, if any. check with 'has_value()' + optional queued_valve(); + + /// returns the number of the valve that is manually selected, if any. check with 'has_value()' + /// this is set by next_valve() and previous_valve() when manual_selection_delay_ > 0 + optional manual_valve(); + + /// returns the number of valves the controller is configured with + size_t number_of_valves(); + + /// returns true if valve number is valid + bool is_a_valid_valve(size_t valve_number); + + /// returns true if the pump the pointer points to is in use + bool pump_in_use(SprinklerSwitch *pump_switch); + + /// switches on/off a pump "safely" by checking that the new state will not conflict with another controller + void set_pump_state(SprinklerSwitch *pump_switch, bool state); + + /// returns the amount of time remaining in seconds for the active valve, if any. check with 'has_value()' + optional time_remaining(); + + /// returns a pointer to a valve's control switch object + SprinklerControllerSwitch *control_switch(size_t valve_number); + + /// returns a pointer to a valve's enable switch object + SprinklerControllerSwitch *enable_switch(size_t valve_number); + + /// returns a pointer to a valve's switch object + SprinklerSwitch *valve_switch(size_t valve_number); + + /// returns a pointer to a valve's pump switch object + SprinklerSwitch *valve_pump_switch(size_t valve_number); + + /// returns a pointer to a valve's pump switch object + SprinklerSwitch *valve_pump_switch_by_pump_index(size_t pump_index); + + protected: + uint32_t hash_base() override; + + /// returns true if valve number is enabled + bool valve_is_enabled_(size_t valve_number); + + /// marks a valve's cycle as complete + void mark_valve_cycle_complete_(size_t valve_number); + + /// returns true if valve's cycle is flagged as complete + bool valve_cycle_complete_(size_t valve_number); + + /// returns the number of the next/previous valve in the vector + size_t next_valve_number_(size_t first_valve); + size_t previous_valve_number_(size_t first_valve); + + /// returns the number of the next valve that should be activated in a full cycle. + /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') + optional next_valve_number_in_cycle_(optional first_valve = nullopt); + + /// loads next_req_ with the next valve that should be activated, including its run duration. + /// if next_req_ already contains a request, nothing is done. after next_req_, + /// queued valves have priority, followed by enabled valves if auto-advance is enabled. + /// if no valve is next (for example, a full cycle is complete), next_req_ is reset via reset(). + void load_next_valve_run_request_(optional first_valve = nullopt); + + /// returns the number of the next/previous valve that should be activated. + /// if no valve is next (cycle is complete), returns no value (check with 'has_value()') + optional next_enabled_incomplete_valve_number_(optional first_valve); + optional previous_enabled_incomplete_valve_number_(optional first_valve); + + /// returns true if any valve is enabled + bool any_valve_is_enabled_(); + + /// loads an available SprinklerValveOperator (valve_op_) based on req and starts it (switches it on). + /// NOTE: if run_duration is zero, the valve's run_duration will be set based on the valve's configuration. + void start_valve_(SprinklerValveRunRequest *req); + + /// turns off/closes all valves, including pump if include_pump is true + void all_valves_off_(bool include_pump = false); + + /// prepares for a full cycle by verifying auto-advance is on as well as one or more valve enable switches. + void prep_full_cycle_(); + + /// resets the cycle state for all valves + void reset_cycle_states_(); + + /// resets resume state + void reset_resume_(); + + /// make a request of the state machine + void fsm_request_(size_t requested_valve, uint32_t requested_run_duration = 0); + + /// kicks the state machine to advance, starting it if it is not already active + void fsm_kick_(); + + /// advance controller state, advancing to target_valve if provided + void fsm_transition_(); + + /// starts up the system from IDLE state + void fsm_transition_from_shutdown_(); + + /// transitions from ACTIVE state to ACTIVE (as in, next valve) or to a SHUTDOWN or IDLE state + void fsm_transition_from_valve_run_(); + + /// starts up the system from IDLE state + void fsm_transition_to_shutdown_(); + + /// return the current FSM state as a string + std::string state_as_str_(SprinklerState state); + + /// Start/cancel/get status of valve timers + void start_timer_(SprinklerTimerIndex timer_index); + bool cancel_timer_(SprinklerTimerIndex timer_index); + /// returns true if the specified timer is active/running + bool timer_active_(SprinklerTimerIndex timer_index); + /// time is converted to milliseconds (ms) for set_timeout() + void set_timer_duration_(SprinklerTimerIndex timer_index, uint32_t time); + /// returns time in milliseconds (ms) + uint32_t timer_duration_(SprinklerTimerIndex timer_index); + std::function timer_cbf_(SprinklerTimerIndex timer_index); + + /// callback functions for timers + void valve_selection_callback_(); + void sm_timer_callback_(); + void pump_stop_delay_callback_(); + + /// Maximum allowed queue size + const uint8_t max_queue_size_{100}; + + /// Pump should be off during valve_open_delay interval + bool pump_switch_off_during_valve_open_delay_{false}; + + /// Sprinkler valve cycle should overlap + bool valve_overlap_{false}; + + /// Pump start/stop delay interval types + bool start_delay_is_valve_delay_{false}; + bool stop_delay_is_valve_delay_{false}; + + /// Pump start/stop delay intervals + uint32_t start_delay_{0}; + uint32_t stop_delay_{0}; + + /// Sprinkler controller state + SprinklerState state_{IDLE}; + + /// The valve run request that is currently active + SprinklerValveRunRequest active_req_; + + /// The number of the manually selected valve currently selected + optional manual_valve_; + + /// The number of the valve to resume from (if paused) + optional paused_valve_; + + /// The next run request for the controller to consume after active_req_ is complete + SprinklerValveRunRequest next_req_; + + /// Set the number of times to repeat a full cycle + optional target_repeats_; + + /// Set from time_remaining() when paused + optional resume_duration_; + + /// Manual switching delay + optional manual_selection_delay_; + + /// Valve switching delay + optional switching_delay_; + + /// Number of times the full cycle has been repeated + uint32_t repeat_count_{0}; + + /// Sprinkler valve run time multiplier value + float multiplier_{1.0}; + + /// Queue of valves to activate next, regardless of auto-advance + std::vector queued_valves_; + + /// Sprinkler valve pump objects + std::vector pump_; + + /// Sprinkler valve objects + std::vector valve_; + + /// Sprinkler valve operator objects + std::vector valve_op_{2}; + + /// Valve control timers + std::vector timer_{ + {this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}, + {this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}}; + + /// Other Sprinkler instances we should be aware of (used to check if pumps are in use) + std::vector other_controllers_; + + /// Switches we'll present to the front end + SprinklerControllerSwitch *auto_adv_sw_{nullptr}; + SprinklerControllerSwitch *controller_sw_{nullptr}; + SprinklerControllerSwitch *queue_enable_sw_{nullptr}; + SprinklerControllerSwitch *reverse_sw_{nullptr}; + + std::unique_ptr> sprinkler_shutdown_action_; + std::unique_ptr> sprinkler_resumeorstart_action_; + + std::unique_ptr> sprinkler_turn_off_automation_; + std::unique_ptr> sprinkler_turn_on_automation_; +}; + +} // namespace sprinkler +} // namespace esphome diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 7b38b1d2c5..c276be2f5a 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -4,15 +4,21 @@ from esphome import pins from esphome.components import display, spi from esphome.const import ( CONF_BACKLIGHT_PIN, - CONF_BRIGHTNESS, CONF_CS_PIN, CONF_DC_PIN, + CONF_HEIGHT, CONF_ID, CONF_LAMBDA, + CONF_MODEL, CONF_RESET_PIN, + CONF_WIDTH, ) from . import st7789v_ns +CONF_EIGHTBITCOLOR = "eightbitcolor" +CONF_OFFSET_HEIGHT = "offset_height" +CONF_OFFSET_WIDTH = "offset_width" + CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["spi"] @@ -21,28 +27,79 @@ ST7789V = st7789v_ns.class_( "ST7789V", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer ) ST7789VRef = ST7789V.operator("ref") +ST7789VModel = st7789v_ns.enum("ST7789VModel") -CONFIG_SCHEMA = ( +MODELS = { + "TTGO_TDISPLAY_135X240": ST7789VModel.ST7789V_MODEL_TTGO_TDISPLAY_135_240, + "ADAFRUIT_FUNHOUSE_240X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, + "ADAFRUIT_RR_280X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_RR_280_240, + "CUSTOM": ST7789VModel.ST7789V_MODEL_CUSTOM, +} + +ST7789V_MODEL = cv.enum(MODELS, upper=True, space="_") + + +def validate_st7789v(config): + if config[CONF_MODEL].upper() == "CUSTOM" and ( + CONF_HEIGHT not in config + or CONF_WIDTH not in config + or CONF_OFFSET_HEIGHT not in config + or CONF_OFFSET_WIDTH not in config + ): + raise cv.Invalid( + f'{CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} and {CONF_OFFSET_WIDTH} must be specified when {CONF_MODEL} is "CUSTOM"' + ) + + if config[CONF_MODEL].upper() != "CUSTOM" and ( + CONF_HEIGHT in config + or CONF_WIDTH in config + or CONF_OFFSET_HEIGHT in config + or CONF_OFFSET_WIDTH in config + ): + raise cv.Invalid( + f'Do not specify {CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} or {CONF_OFFSET_WIDTH} when using {CONF_MODEL} that is not "CUSTOM"' + ) + return config + + +CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ST7789V), + cv.Required(CONF_MODEL): ST7789V_MODEL, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, + cv.Optional(CONF_HEIGHT): cv.int_, + cv.Optional(CONF_WIDTH): cv.int_, + cv.Optional(CONF_OFFSET_HEIGHT): cv.int_, + cv.Optional(CONF_OFFSET_WIDTH): cv.int_, } ) .extend(cv.polling_component_schema("5s")) - .extend(spi.spi_device_schema()) + .extend(spi.spi_device_schema()), + validate_st7789v, ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + await display.register_display(var, config) await spi.register_spi_device(var, config) + cg.add(var.set_model(config[CONF_MODEL])) + + if config[CONF_MODEL].upper() == "CUSTOM": + cg.add(var.set_height(config[CONF_HEIGHT])) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_offset_height(config[CONF_OFFSET_HEIGHT])) + cg.add(var.set_offset_width(config[CONF_OFFSET_WIDTH])) + + cg.add(var.set_eightbitcolor(config[CONF_EIGHTBITCOLOR])) + dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) @@ -58,5 +115,3 @@ async def to_code(config): config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) - - await display.register_display(var, config) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 471ad6664c..8a4fcfb179 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -102,7 +102,7 @@ void ST7789V::setup() { this->write_command_(ST7789_INVON); // Clear display - ensures we do not see garbage at power-on - this->draw_filled_rect_(0, 0, 239, 319, 0x0000); + this->draw_filled_rect_(0, 0, this->get_width_internal(), this->get_height_internal(), 0x0000); delay(120); // NOLINT @@ -117,6 +117,12 @@ void ST7789V::setup() { void ST7789V::dump_config() { LOG_DISPLAY("", "SPI ST7789V", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->model_ == ST7789V_MODEL_CUSTOM) { + ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_height_); + ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_width_); + } + ESP_LOGCONFIG(TAG, " 8-bit color mode: %s", YESNO(this->eightbitcolor_)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); @@ -131,13 +137,41 @@ void ST7789V::update() { this->write_display_data(); } -void ST7789V::loop() {} +void ST7789V::set_model(ST7789VModel model) { + this->model_ = model; + + switch (this->model_) { + case ST7789V_MODEL_TTGO_TDISPLAY_135_240: + this->height_ = 240; + this->width_ = 135; + this->offset_height_ = 52; + this->offset_width_ = 40; + break; + + case ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240: + this->height_ = 240; + this->width_ = 240; + this->offset_height_ = 0; + this->offset_width_ = 0; + break; + + case ST7789V_MODEL_ADAFRUIT_RR_280_240: + this->height_ = 280; + this->width_ = 240; + this->offset_height_ = 0; + this->offset_width_ = 20; + break; + + default: + break; + } +} void ST7789V::write_display_data() { - uint16_t x1 = 52; // _offsetx - uint16_t x2 = 186; // _offsetx - uint16_t y1 = 40; // _offsety - uint16_t y2 = 279; // _offsety + uint16_t x1 = this->offset_height_; + uint16_t x2 = x1 + get_width_internal() - 1; + uint16_t y1 = this->offset_width_; + uint16_t y2 = y1 + get_height_internal() - 1; this->enable(); @@ -156,7 +190,19 @@ void ST7789V::write_display_data() { this->write_byte(ST7789_RAMWR); this->dc_pin_->digital_write(true); - this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->eightbitcolor_) { + for (int line = 0; line < this->get_buffer_length_(); line = line + this->get_width_internal()) { + for (int index = 0; index < this->get_width_internal(); ++index) { + auto color = display::ColorUtil::color_to_565( + display::ColorUtil::to_color(this->buffer_[index + line], display::ColorOrder::COLOR_ORDER_RGB, + display::ColorBitness::COLOR_BITNESS_332, true)); + this->write_byte((color >> 8) & 0xff); + this->write_byte(color & 0xff); + } + } + } else { + this->write_array(this->buffer_, this->get_buffer_length_()); + } this->disable(); } @@ -219,15 +265,10 @@ void ST7789V::write_color_(uint16_t color, uint16_t size) { return write_array(byte, size * 2); } -int ST7789V::get_height_internal() { - return 240; // 320; -} - -int ST7789V::get_width_internal() { - return 135; // 240; -} - size_t ST7789V::get_buffer_length_() { + if (this->eightbitcolor_) { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()); + } return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; } @@ -238,7 +279,6 @@ size_t ST7789V::get_buffer_length_() { // y2: End Y coordinate // color: color void ST7789V::draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { - // ESP_LOGD(TAG,"offset(x)=%d offset(y)=%d",dev->_offsetx,dev->_offsety); this->enable(); this->dc_pin_->digital_write(false); this->write_byte(ST7789_CASET); // set column(x) address @@ -263,11 +303,29 @@ void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; - auto color565 = display::ColorUtil::color_to_565(color); + if (this->eightbitcolor_) { + auto color332 = display::ColorUtil::color_to_332(color); + uint32_t pos = (x + y * this->get_width_internal()); + this->buffer_[pos] = color332; + } else { + auto color565 = display::ColorUtil::color_to_565(color); + uint32_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; + } +} - uint16_t pos = (x + y * this->get_width_internal()) * 2; - this->buffer_[pos++] = (color565 >> 8) & 0xff; - this->buffer_[pos] = color565 & 0xff; +const char *ST7789V::model_str_() { + switch (this->model_) { + case ST7789V_MODEL_TTGO_TDISPLAY_135_240: + return "TTGO T-Display 135x240"; + case ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240: + return "Adafruit Funhouse 240x240"; + case ST7789V_MODEL_ADAFRUIT_RR_280_240: + return "Adafruit Round-Rectangular 280x240"; + default: + return "Custom"; + } } } // namespace st7789v diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index e655d22e2b..96e97c9d78 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -7,8 +7,12 @@ namespace esphome { namespace st7789v { -static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; +enum ST7789VModel { + ST7789V_MODEL_TTGO_TDISPLAY_135_240, + ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, + ST7789V_MODEL_ADAFRUIT_RR_280_240, + ST7789V_MODEL_CUSTOM +}; static const uint8_t ST7789_NOP = 0x00; // No Operation static const uint8_t ST7789_SWRESET = 0x01; // Software Reset @@ -110,29 +114,42 @@ static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; class ST7789V : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevice { + spi::DATA_RATE_10MHZ> { public: + void set_model(ST7789VModel model); void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } + void set_eightbitcolor(bool eightbitcolor) { this->eightbitcolor_ = eightbitcolor; } + void set_height(uint32_t height) { this->height_ = height; } + void set_width(uint16_t width) { this->width_ = width; } + void set_offset_height(uint32_t offset_height) { this->offset_height_ = offset_height; } + void set_offset_width(uint16_t offset_width) { this->offset_width_ = offset_width; } + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void setup() override; void dump_config() override; float get_setup_priority() const override; void update() override; - void loop() override; void write_display_data(); display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } protected: - GPIOPin *dc_pin_; + ST7789VModel model_{ST7789V_MODEL_TTGO_TDISPLAY_135_240}; + GPIOPin *dc_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; GPIOPin *backlight_pin_{nullptr}; + bool eightbitcolor_{false}; + uint16_t height_{0}; + uint16_t width_{0}; + uint16_t offset_height_{0}; + uint16_t offset_width_{0}; + void init_reset_(); void backlight_(bool onoff); void write_command_(uint8_t value); @@ -140,13 +157,15 @@ class ST7789V : public PollingComponent, void write_addr_(uint16_t addr1, uint16_t addr2); void write_color_(uint16_t color, uint16_t size); - int get_height_internal() override; - int get_width_internal() override; + int get_height_internal() override { return this->height_; } + int get_width_internal() override { return this->width_; } size_t get_buffer_length_(); void draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); void draw_absolute_pixel_internal(int x, int y, Color color) override; + + const char *model_str_(); }; } // namespace st7789v diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 71a16439cd..54ad2b852e 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -5,6 +5,7 @@ from esphome.automation import Condition, maybe_simple_id from esphome.components import mqtt from esphome.const import ( CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, CONF_ID, CONF_INVERTED, CONF_MQTT_ID, @@ -16,6 +17,7 @@ from esphome.const import ( DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] @@ -45,6 +47,8 @@ SwitchTurnOffTrigger = switch_ns.class_( icon = cv.icon +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True) + SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -60,10 +64,40 @@ SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), } ), - cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, } ) +_UNDEF = object() + + +def switch_schema( + class_: MockObjClass = _UNDEF, + *, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, +): + schema = SWITCH_SCHEMA + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) + return schema + async def setup_switch_core_(var, config): await setup_entity(var, config) @@ -92,6 +126,12 @@ async def register_switch(var, config): await setup_switch_core_(var, config) +async def new_switch(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_switch(var, config) + return var + + SWITCH_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(Switch), diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 879ced2fb3..faef940125 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_MODE, CONF_INVERTED, CONF_OUTPUT, + CONF_PULLDOWN, CONF_PULLUP, ) @@ -74,6 +75,10 @@ def validate_mode(value): raise cv.Invalid("Mode must be either input or output") if value[CONF_PULLUP] and not value[CONF_INPUT]: raise cv.Invalid("Pullup only available with input") + if value[CONF_PULLDOWN] and not value[CONF_INPUT]: + raise cv.Invalid("Pulldown only available with input") + if value[CONF_PULLUP] and value[CONF_PULLDOWN]: + raise cv.Invalid("Can only have one of pullup or pulldown") return value @@ -87,6 +92,7 @@ SX1509_PIN_SCHEMA = cv.All( { cv.Optional(CONF_INPUT, default=False): cv.boolean, cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, cv.Optional(CONF_OUTPUT, default=False): cv.boolean, }, validate_mode, diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 16540d0dbf..60cbae6aa6 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -70,26 +70,6 @@ void SX1509Component::digital_write(uint8_t pin, bool bit_value) { temp_reg_data &= ~(1 << pin); } this->write_byte_16(REG_DATA_B, temp_reg_data); - } else { - // Otherwise the pin is an input, pull-up/down - uint16_t temp_pullup = 0; - this->read_byte_16(REG_PULL_UP_B, &temp_pullup); - uint16_t temp_pull_down = 0; - this->read_byte_16(REG_PULL_DOWN_B, &temp_pull_down); - - if (bit_value) { - // if HIGH, do pull-up, disable pull-down - temp_pullup |= (1 << pin); - temp_pull_down &= ~(1 << pin); - this->write_byte_16(REG_PULL_UP_B, temp_pullup); - this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down); - } else { - // If LOW do pull-down, disable pull-up - temp_pull_down |= (1 << pin); - temp_pullup &= ~(1 << pin); - this->write_byte_16(REG_PULL_UP_B, temp_pullup); - this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down); - } } } @@ -99,11 +79,28 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->ddr_mask_ &= ~(1 << pin); } else { this->ddr_mask_ |= (1 << pin); + + uint16_t temp_pullup; + this->read_byte_16(REG_PULL_UP_B, &temp_pullup); + uint16_t temp_pulldown; + this->read_byte_16(REG_PULL_DOWN_B, &temp_pulldown); + + if (flags & gpio::FLAG_PULLUP) { + temp_pullup |= (1 << pin); + } else { + temp_pullup &= ~(1 << pin); + } + + if (flags & gpio::FLAG_PULLDOWN) { + temp_pulldown |= (1 << pin); + } else { + temp_pulldown &= ~(1 << pin); + } + + this->write_byte_16(REG_PULL_UP_B, temp_pullup); + this->write_byte_16(REG_PULL_DOWN_B, temp_pulldown); } this->write_byte_16(REG_DIR_B, this->ddr_mask_); - - if (flags & gpio::FLAG_PULLUP) - digital_write(pin, true); } void SX1509Component::setup_led_driver(uint8_t pin) { @@ -114,10 +111,6 @@ void SX1509Component::setup_led_driver(uint8_t pin) { temp_word |= (1 << pin); this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); - this->read_byte_16(REG_PULL_UP_B, &temp_word); - temp_word &= ~(1 << pin); - this->write_byte_16(REG_PULL_UP_B, temp_word); - this->ddr_mask_ &= ~(1 << pin); // 0=output this->write_byte_16(REG_DIR_B, this->ddr_mask_); diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index f2f382ceaa..dd3dbddc43 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -189,8 +189,8 @@ async def register_text_sensor(var, config): await setup_text_sensor_core_(var, config) -async def new_text_sensor(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_text_sensor(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_text_sensor(var, config) return var diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index c9231370ba..a5498dc53d 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -163,7 +163,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly void change_custom_preset_(const std::string &custom_preset); - /// Applies the temperature, mode, fan, and swing modes of the provded config. + /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset void change_preset_internal_(const ThermostatClimateTargetTempConfig &config); diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 2d73d0aef9..b2be11611d 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -126,10 +126,10 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): ) begin, end = data begin_n = _parse_cron_int( - begin, special_mapping, "Number for time range must be integer, " "got {}" + begin, special_mapping, "Number for time range must be integer, got {}" ) end_n = _parse_cron_int( - end, special_mapping, "Number for time range must be integer, " "got {}" + end, special_mapping, "Number for time range must be integer, got {}" ) if end_n < begin_n: return set(range(end_n, max_value + 1)) | set(range(min_value, begin_n + 1)) @@ -139,7 +139,7 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): _parse_cron_int( part, special_mapping, - "Number for time expression must be an " "integer, got {}", + "Number for time expression must be an integer, got {}", ) } diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 3698563aff..45e9c03c1d 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -17,7 +17,6 @@ from esphome.core.entity_helpers import inherit_property_from DEPENDENCIES = ["time"] CONF_POWER_ID = "power_id" -CONF_MIN_SAVE_INTERVAL = "min_save_interval" total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy") TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod") TOTAL_DAILY_ENERGY_METHODS = { @@ -49,9 +48,9 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), cv.Optional(CONF_RESTORE, default=True): cv.boolean, - cv.Optional( - CONF_MIN_SAVE_INTERVAL, default="0s" - ): cv.positive_time_period_milliseconds, + cv.Optional("min_save_interval"): cv.invalid( + "`min_save_interval` was removed in 2022.6.0. Please use the `preferences` -> `flash_write_interval` to adjust." + ), cv.Optional(CONF_METHOD, default="right"): cv.enum( TOTAL_DAILY_ENERGY_METHODS, lower=True ), @@ -90,5 +89,4 @@ async def to_code(config): time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) cg.add(var.set_restore(config[CONF_RESTORE])) - cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) cg.add(var.set_method(config[CONF_METHOD])) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 3746301715..7c316c495d 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -16,7 +16,6 @@ void TotalDailyEnergy::setup() { this->publish_state_and_save(initial_value); this->last_update_ = millis(); - this->last_save_ = this->last_update_; this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); } @@ -43,12 +42,9 @@ void TotalDailyEnergy::loop() { void TotalDailyEnergy::publish_state_and_save(float state) { this->total_energy_ = state; this->publish_state(state); - const uint32_t now = millis(); - if (now - this->last_save_ < this->min_save_interval_) { - return; + if (this->restore_) { + this->pref_.save(&state); } - this->last_save_ = now; - this->pref_.save(&state); } void TotalDailyEnergy::process_new_state_(float state) { diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index a35edfd11b..a40c56a7db 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -18,7 +18,6 @@ enum TotalDailyEnergyMethod { class TotalDailyEnergy : public sensor::Sensor, public Component { public: void set_restore(bool restore) { restore_ = restore; } - void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_time(time::RealTimeClock *time) { time_ = time; } void set_parent(Sensor *parent) { parent_ = parent; } void set_method(TotalDailyEnergyMethod method) { method_ = method; } @@ -39,7 +38,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { uint16_t last_day_of_year_{}; uint32_t last_update_{0}; uint32_t last_save_{0}; - uint32_t min_save_interval_{0}; bool restore_; float total_energy_{0.0f}; float last_power_state_{0.0f}; diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index 6e84bbe450..4f9b6d09df 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -38,7 +38,7 @@ class UARTDebugger : public Component, public Triggerafter_delimiter_.push_back(byte); } diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 171484f6f2..f851cf6d73 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -257,6 +257,7 @@ void VL53L0XSensor::setup() { ESP_LOGD(TAG, "'%s' - setup END", this->name_.c_str()); } + void VL53L0XSensor::update() { if (this->initiated_read_ || this->waiting_for_interrupt_) { this->publish_state(NAN); @@ -280,6 +281,7 @@ void VL53L0XSensor::update() { this->initiated_read_ = true; // wait for timeout } + void VL53L0XSensor::loop() { if (this->initiated_read_) { if (reg(0x00).get() & 0x01) { @@ -311,5 +313,222 @@ void VL53L0XSensor::loop() { } } +uint32_t VL53L0XSensor::get_measurement_timing_budget_() { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1910; + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + // "Start and end overhead times always present" + uint32_t budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) + budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + + if (enables.dss) { + budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + } else if (enables.msrc) { + budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + } + + if (enables.pre_range) + budget_us += (timeouts.pre_range_us + pre_range_overhead); + + if (enables.final_range) + budget_us += (timeouts.final_range_us + final_range_overhead); + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + return budget_us; +} + +bool VL53L0XSensor::set_measurement_timing_budget_(uint32_t budget_us) { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1320; // note that this is different than the value in get_ + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + uint32_t min_timing_budget = 20000; + + if (budget_us < min_timing_budget) { + return false; + } + + uint32_t used_budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + } + + if (enables.dss) { + used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + } else if (enables.msrc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + } + + if (enables.pre_range) { + used_budget_us += (timeouts.pre_range_us + pre_range_overhead); + } + + if (enables.final_range) { + used_budget_us += final_range_overhead; + + // "Note that the final range timeout is determined by the timing + // budget and the sum of all other timeouts within the sequence. + // If there is no room for the final range timeout, then an error + // will be set. Otherwise the remaining time will be applied to + // the final range." + + if (used_budget_us > budget_us) { + // "Requested timeout too big." + return false; + } + + uint32_t final_range_timeout_us = budget_us - used_budget_us; + + // set_sequence_step_timeout() begin + // (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE) + + // "For the final range timeout, the pre-range timeout + // must be added. To do this both final and pre-range + // timeouts must be expressed in macro periods MClks + // because they have different vcsel periods." + + uint16_t final_range_timeout_mclks = + timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks); + + if (enables.pre_range) { + final_range_timeout_mclks += timeouts.pre_range_mclks; + } + + write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks)); + + // set_sequence_step_timeout() end + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + } + return true; +} + +void VL53L0XSensor::get_sequence_step_enables_(SequenceStepEnables *enables) { + uint8_t sequence_config = reg(0x01).get(); + enables->tcc = (sequence_config >> 4) & 0x1; + enables->dss = (sequence_config >> 3) & 0x1; + enables->msrc = (sequence_config >> 2) & 0x1; + enables->pre_range = (sequence_config >> 6) & 0x1; + enables->final_range = (sequence_config >> 7) & 0x1; +} + +void VL53L0XSensor::get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) { + timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE); + + timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1; + timeouts->msrc_dss_tcc_us = + timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks); + + uint16_t value; + read_byte_16(0x51, &value); + timeouts->pre_range_mclks = decode_timeout_(value); + timeouts->pre_range_us = + timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks); + + timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE); + + read_byte_16(0x71, &value); + timeouts->final_range_mclks = decode_timeout_(value); + + if (enables->pre_range) { + timeouts->final_range_mclks -= timeouts->pre_range_mclks; + } + + timeouts->final_range_us = + timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks); +} + +uint8_t VL53L0XSensor::get_vcsel_pulse_period_(VcselPeriodType type) { + uint8_t vcsel; + if (type == VCSEL_PERIOD_PRE_RANGE) { + vcsel = reg(0x50).get(); + } else if (type == VCSEL_PERIOD_FINAL_RANGE) { + vcsel = reg(0x70).get(); + } else { + return 255; + } + + return (vcsel + 1) << 1; +} + +uint32_t VL53L0XSensor::get_macro_period_(uint8_t vcsel_period_pclks) { + return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL; +} + +uint32_t VL53L0XSensor::timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000; +} + +uint32_t VL53L0XSensor::timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns); +} + +uint16_t VL53L0XSensor::decode_timeout_(uint16_t reg_val) { + // format: "(LSByte * 2^MSByte) + 1" + uint8_t msb = (reg_val >> 8) & 0xFF; + uint8_t lsb = (reg_val >> 0) & 0xFF; + return (uint16_t(lsb) << msb) + 1; +} + +uint16_t VL53L0XSensor::encode_timeout_(uint16_t timeout_mclks) { + // format: "(LSByte * 2^MSByte) + 1" + uint32_t ls_byte = 0; + uint16_t ms_byte = 0; + + if (timeout_mclks <= 0) + return 0; + + ls_byte = timeout_mclks - 1; + + while ((ls_byte & 0xFFFFFF00) > 0) { + ls_byte >>= 1; + ms_byte++; + } + + return (ms_byte << 8) | (ls_byte & 0xFF); +} + +bool VL53L0XSensor::perform_single_ref_calibration_(uint8_t vhv_init_byte) { + reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP + + uint32_t start = millis(); + while ((reg(0x13).get() & 0x07) == 0) { + if (millis() - start > 1000) + return false; + yield(); + } + + reg(0x0B) = 0x01; + reg(0x00) = 0x00; + + return true; +} + } // namespace vl53l0x } // namespace esphome diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 85b5e3b31d..971fb458bb 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -21,6 +21,8 @@ struct SequenceStepTimeouts { uint32_t msrc_dss_tcc_us, pre_range_us, final_range_us; }; +enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE }; + class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: VL53L0XSensor(); @@ -39,222 +41,20 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void set_enable_pin(GPIOPin *enable) { this->enable_pin_ = enable; } protected: - uint32_t get_measurement_timing_budget_() { - SequenceStepEnables enables{}; - SequenceStepTimeouts timeouts{}; + uint32_t get_measurement_timing_budget_(); + bool set_measurement_timing_budget_(uint32_t budget_us); + void get_sequence_step_enables_(SequenceStepEnables *enables); + void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts); + uint8_t get_vcsel_pulse_period_(VcselPeriodType type); + uint32_t get_macro_period_(uint8_t vcsel_period_pclks); - uint16_t start_overhead = 1910; - uint16_t end_overhead = 960; - uint16_t msrc_overhead = 660; - uint16_t tcc_overhead = 590; - uint16_t dss_overhead = 690; - uint16_t pre_range_overhead = 660; - uint16_t final_range_overhead = 550; + uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks); + uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks); - // "Start and end overhead times always present" - uint32_t budget_us = start_overhead + end_overhead; + uint16_t decode_timeout_(uint16_t reg_val); + uint16_t encode_timeout_(uint16_t timeout_mclks); - get_sequence_step_enables_(&enables); - get_sequence_step_timeouts_(&enables, &timeouts); - - if (enables.tcc) - budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); - - if (enables.dss) { - budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); - } else if (enables.msrc) { - budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); - } - - if (enables.pre_range) - budget_us += (timeouts.pre_range_us + pre_range_overhead); - - if (enables.final_range) - budget_us += (timeouts.final_range_us + final_range_overhead); - - measurement_timing_budget_us_ = budget_us; // store for internal reuse - return budget_us; - } - - bool set_measurement_timing_budget_(uint32_t budget_us) { - SequenceStepEnables enables{}; - SequenceStepTimeouts timeouts{}; - - uint16_t start_overhead = 1320; // note that this is different than the value in get_ - uint16_t end_overhead = 960; - uint16_t msrc_overhead = 660; - uint16_t tcc_overhead = 590; - uint16_t dss_overhead = 690; - uint16_t pre_range_overhead = 660; - uint16_t final_range_overhead = 550; - - uint32_t min_timing_budget = 20000; - - if (budget_us < min_timing_budget) { - return false; - } - - uint32_t used_budget_us = start_overhead + end_overhead; - - get_sequence_step_enables_(&enables); - get_sequence_step_timeouts_(&enables, &timeouts); - - if (enables.tcc) { - used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); - } - - if (enables.dss) { - used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); - } else if (enables.msrc) { - used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); - } - - if (enables.pre_range) { - used_budget_us += (timeouts.pre_range_us + pre_range_overhead); - } - - if (enables.final_range) { - used_budget_us += final_range_overhead; - - // "Note that the final range timeout is determined by the timing - // budget and the sum of all other timeouts within the sequence. - // If there is no room for the final range timeout, then an error - // will be set. Otherwise the remaining time will be applied to - // the final range." - - if (used_budget_us > budget_us) { - // "Requested timeout too big." - return false; - } - - uint32_t final_range_timeout_us = budget_us - used_budget_us; - - // set_sequence_step_timeout() begin - // (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE) - - // "For the final range timeout, the pre-range timeout - // must be added. To do this both final and pre-range - // timeouts must be expressed in macro periods MClks - // because they have different vcsel periods." - - uint16_t final_range_timeout_mclks = - timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks); - - if (enables.pre_range) { - final_range_timeout_mclks += timeouts.pre_range_mclks; - } - - write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks)); - - // set_sequence_step_timeout() end - - measurement_timing_budget_us_ = budget_us; // store for internal reuse - } - return true; - } - - void get_sequence_step_enables_(SequenceStepEnables *enables) { - uint8_t sequence_config = reg(0x01).get(); - enables->tcc = (sequence_config >> 4) & 0x1; - enables->dss = (sequence_config >> 3) & 0x1; - enables->msrc = (sequence_config >> 2) & 0x1; - enables->pre_range = (sequence_config >> 6) & 0x1; - enables->final_range = (sequence_config >> 7) & 0x1; - } - - enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE }; - - void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) { - timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE); - - timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1; - timeouts->msrc_dss_tcc_us = - timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks); - - uint16_t value; - read_byte_16(0x51, &value); - timeouts->pre_range_mclks = decode_timeout_(value); - timeouts->pre_range_us = - timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks); - - timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE); - - read_byte_16(0x71, &value); - timeouts->final_range_mclks = decode_timeout_(value); - - if (enables->pre_range) { - timeouts->final_range_mclks -= timeouts->pre_range_mclks; - } - - timeouts->final_range_us = - timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks); - } - - uint8_t get_vcsel_pulse_period_(VcselPeriodType type) { - uint8_t vcsel; - if (type == VCSEL_PERIOD_PRE_RANGE) { - vcsel = reg(0x50).get(); - } else if (type == VCSEL_PERIOD_FINAL_RANGE) { - vcsel = reg(0x70).get(); - } else { - return 255; - } - - return (vcsel + 1) << 1; - } - - uint32_t get_macro_period_(uint8_t vcsel_period_pclks) { - return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL; - } - - uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) { - uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); - return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000; - } - uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) { - uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); - return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns); - } - - uint16_t decode_timeout_(uint16_t reg_val) { - // format: "(LSByte * 2^MSByte) + 1" - uint8_t msb = (reg_val >> 8) & 0xFF; - uint8_t lsb = (reg_val >> 0) & 0xFF; - return (uint16_t(lsb) << msb) + 1; - } - uint16_t encode_timeout_(uint16_t timeout_mclks) { - // format: "(LSByte * 2^MSByte) + 1" - uint32_t ls_byte = 0; - uint16_t ms_byte = 0; - - if (timeout_mclks <= 0) - return 0; - - ls_byte = timeout_mclks - 1; - - while ((ls_byte & 0xFFFFFF00) > 0) { - ls_byte >>= 1; - ms_byte++; - } - - return (ms_byte << 8) | (ls_byte & 0xFF); - } - - bool perform_single_ref_calibration_(uint8_t vhv_init_byte) { - reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP - - uint32_t start = millis(); - while ((reg(0x13).get() & 0x07) == 0) { - if (millis() - start > 1000) - return false; - yield(); - } - - reg(0x0B) = 0x01; - reg(0x00) = 0x00; - - return true; - } + bool perform_single_ref_calibration_(uint8_t vhv_init_byte); float signal_rate_limit_; bool long_range_; diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h index 719a804d0c..75c7130151 100644 --- a/esphome/components/web_server/server_index.h +++ b/esphome/components/web_server/server_index.h @@ -6,568 +6,580 @@ namespace esphome { namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xbd, 0x7d, 0xd9, 0x76, 0xe3, 0xc6, 0x92, 0xe0, 0xf3, - 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x10, 0xac, 0x56, 0x01, 0x16, 0x08, 0x91, 0x54, 0x6d, 0x06, 0x05, 0xf2, 0xca, 0x55, - 0xe5, 0x5b, 0x65, 0xd7, 0xe6, 0x92, 0xaa, 0xbc, 0xc8, 0x74, 0x09, 0x22, 0x93, 0x22, 0x5c, 0x20, 0x40, 0x03, 0x49, - 0x2d, 0xa6, 0xd0, 0xa7, 0x9f, 0xfa, 0x69, 0xce, 0x99, 0xf5, 0xa1, 0x5f, 0xe6, 0xf4, 0xcb, 0x7c, 0xc4, 0x7c, 0xcf, - 0xfd, 0x81, 0xe9, 0x4f, 0x98, 0x88, 0xc8, 0x05, 0x09, 0x90, 0x5a, 0xec, 0xf6, 0xdc, 0x53, 0x8b, 0x80, 0x5c, 0x23, - 0x23, 0x23, 0x63, 0x4f, 0x68, 0x6f, 0x63, 0x9c, 0x8d, 0xf8, 0xe5, 0x9c, 0x59, 0x53, 0x3e, 0x4b, 0xfa, 0x7b, 0xf2, - 0x7f, 0x16, 0x8d, 0xfb, 0x7b, 0x49, 0x9c, 0x7e, 0xb2, 0x72, 0x96, 0x84, 0xf1, 0x28, 0x4b, 0xad, 0x69, 0xce, 0x26, - 0xe1, 0x38, 0xe2, 0x51, 0x10, 0xcf, 0xa2, 0x53, 0x66, 0xed, 0xf4, 0xf7, 0x66, 0x8c, 0x47, 0xd6, 0x68, 0x1a, 0xe5, - 0x05, 0xe3, 0xe1, 0xfb, 0xc3, 0xaf, 0x5a, 0x8f, 0xfb, 0x7b, 0xc5, 0x28, 0x8f, 0xe7, 0xdc, 0xc2, 0x21, 0xc3, 0x59, - 0x36, 0x5e, 0x24, 0xac, 0x7f, 0x16, 0xe5, 0xd6, 0x3e, 0x0b, 0xdf, 0x9c, 0xfc, 0xc2, 0x46, 0xdc, 0x1f, 0xb3, 0x49, - 0x9c, 0xb2, 0xb7, 0x79, 0x36, 0x67, 0x39, 0xbf, 0xf4, 0x2e, 0xd6, 0x57, 0xc4, 0xac, 0xf0, 0x9e, 0xe9, 0xaa, 0x53, - 0xc6, 0xdf, 0x9c, 0xa7, 0xaa, 0xcf, 0x53, 0x26, 0x26, 0xc9, 0xf2, 0xc2, 0xe3, 0xd7, 0xb4, 0x39, 0xb8, 0x9c, 0x9d, - 0x64, 0x49, 0xe1, 0x1d, 0xe8, 0xfa, 0x79, 0x9e, 0xf1, 0x0c, 0xc1, 0xf2, 0xa7, 0x51, 0x61, 0xb4, 0xf4, 0x3e, 0xad, - 0x69, 0x32, 0x97, 0x95, 0x2f, 0x8a, 0x67, 0xe9, 0x62, 0xc6, 0xf2, 0xe8, 0x24, 0x61, 0x5e, 0xc1, 0x42, 0x87, 0x79, - 0xdc, 0x8b, 0xdd, 0xb0, 0xcf, 0xad, 0x38, 0xb5, 0xd8, 0x60, 0x9f, 0x51, 0xc9, 0x92, 0xe9, 0x56, 0xc1, 0x46, 0xdb, - 0x03, 0x74, 0x4d, 0xe2, 0xd3, 0x85, 0x7e, 0x3f, 0xcf, 0x63, 0xae, 0x9e, 0xcf, 0xa2, 0x64, 0xc1, 0x82, 0xb8, 0x74, - 0x03, 0x76, 0xc4, 0x87, 0x61, 0xec, 0x3d, 0xa1, 0x41, 0x61, 0xc8, 0xe5, 0x24, 0xcb, 0x1d, 0xc4, 0x55, 0x8c, 0x63, - 0xf3, 0xab, 0x2b, 0x87, 0x87, 0xcb, 0xd2, 0x75, 0x0f, 0x98, 0x3f, 0x8a, 0x92, 0xc4, 0xc1, 0x89, 0xb7, 0xb6, 0x0a, - 0x9c, 0x31, 0xf6, 0xf8, 0x51, 0x3c, 0x74, 0x7b, 0xf1, 0xc4, 0xe1, 0xcc, 0xad, 0xfa, 0x65, 0x13, 0x8b, 0x33, 0x87, - 0xbb, 0xee, 0xa7, 0xeb, 0xfb, 0xe4, 0x8c, 0x2f, 0x72, 0x80, 0xbd, 0xf4, 0xde, 0xa8, 0x99, 0x2f, 0xb0, 0xfe, 0x19, - 0x75, 0xec, 0x01, 0xec, 0x05, 0xb7, 0x3e, 0x84, 0xe7, 0x71, 0x3a, 0xce, 0xce, 0xfd, 0x83, 0x69, 0x04, 0x3f, 0xde, - 0x65, 0x19, 0xdf, 0xda, 0x72, 0xce, 0xb2, 0x78, 0x6c, 0xb5, 0xc3, 0xd0, 0xac, 0xbc, 0x7c, 0x72, 0x70, 0x70, 0x75, - 0xd5, 0x28, 0xf0, 0xd3, 0x88, 0xc7, 0x67, 0x4c, 0x74, 0x06, 0x00, 0x6c, 0xf8, 0x39, 0xe7, 0x6c, 0x7c, 0xc0, 0x2f, - 0x13, 0x28, 0x65, 0x8c, 0x17, 0x36, 0xac, 0xf1, 0x69, 0x36, 0x02, 0xb4, 0xa5, 0x06, 0xe2, 0xa1, 0x69, 0xce, 0xe6, - 0x49, 0x34, 0x62, 0x58, 0x0f, 0x23, 0x55, 0x3d, 0xaa, 0x46, 0xde, 0x57, 0xa1, 0xd8, 0x5e, 0xc7, 0xf5, 0x62, 0x16, - 0xa6, 0xec, 0xdc, 0x7a, 0x15, 0xcd, 0x7b, 0xa3, 0x24, 0x2a, 0x0a, 0xa0, 0xd7, 0x25, 0x2d, 0x21, 0x5f, 0x8c, 0x80, - 0x40, 0x68, 0x81, 0x4b, 0x44, 0xd3, 0x34, 0x2e, 0xfc, 0x8f, 0x9b, 0xa3, 0xa2, 0x78, 0xc7, 0x8a, 0x45, 0xc2, 0x37, - 0x43, 0xd8, 0x0b, 0xbe, 0x11, 0x86, 0x5f, 0xb9, 0x7c, 0x9a, 0x67, 0xe7, 0xd6, 0xb3, 0x3c, 0x87, 0xe6, 0x36, 0x4c, - 0x29, 0x1a, 0x58, 0x71, 0x61, 0xa5, 0x19, 0xb7, 0xf4, 0x60, 0xb8, 0x81, 0xbe, 0xf5, 0xbe, 0x60, 0xd6, 0xf1, 0x22, - 0x2d, 0xa2, 0x09, 0x83, 0xa6, 0xc7, 0x56, 0x96, 0x5b, 0xc7, 0x30, 0xe8, 0x31, 0x6c, 0x59, 0xc1, 0xe1, 0xd4, 0xf8, - 0xb6, 0xdb, 0xa3, 0xb9, 0xa0, 0xf0, 0x90, 0x5d, 0xf0, 0x90, 0x95, 0x40, 0x98, 0x56, 0xa1, 0x97, 0xe1, 0xb8, 0xcb, - 0x04, 0x0a, 0x58, 0x18, 0x33, 0x24, 0x59, 0xc7, 0x6c, 0xac, 0x37, 0xe7, 0xc3, 0xd6, 0x96, 0xc6, 0x35, 0xe0, 0xc4, - 0x81, 0xb6, 0x45, 0xa3, 0xad, 0x27, 0x16, 0x5e, 0x43, 0x91, 0xeb, 0x31, 0x5f, 0xa2, 0xef, 0xe0, 0x32, 0x1d, 0xd5, - 0xc7, 0x86, 0xca, 0x92, 0x67, 0x07, 0x3c, 0x8f, 0xd3, 0x53, 0x00, 0x42, 0xce, 0x64, 0x36, 0x29, 0x4b, 0xb1, 0xf9, - 0x4f, 0x58, 0xc8, 0xc2, 0x3e, 0x8e, 0x9e, 0x33, 0xc7, 0x2e, 0xa8, 0x87, 0x1d, 0x86, 0x88, 0x7a, 0x20, 0x30, 0x36, - 0x60, 0x01, 0xdb, 0xb6, 0x6d, 0xef, 0x2b, 0xd7, 0x3b, 0x47, 0x0a, 0xf2, 0x7d, 0x9f, 0xc8, 0x57, 0x74, 0x8e, 0xc3, - 0x0e, 0x02, 0xed, 0x27, 0x2c, 0x3d, 0xe5, 0xd3, 0x01, 0x3b, 0x6a, 0x0f, 0x03, 0x0e, 0x50, 0x8d, 0x17, 0x23, 0xe6, - 0x20, 0x3d, 0x7a, 0x05, 0x1e, 0x9f, 0x6d, 0x07, 0xa6, 0xc0, 0x8d, 0xd9, 0xa0, 0x35, 0xd6, 0xb6, 0xc6, 0x55, 0x24, - 0xaa, 0x00, 0x43, 0x3a, 0xb7, 0xe1, 0x84, 0x9d, 0xb0, 0xdc, 0x80, 0x43, 0x37, 0xeb, 0xd5, 0x76, 0x70, 0x01, 0x3b, - 0x04, 0xfd, 0xac, 0xc9, 0x22, 0x1d, 0xf1, 0x18, 0x18, 0x97, 0xbd, 0x0d, 0xe0, 0x8a, 0x9d, 0xd3, 0x1b, 0x67, 0xbb, - 0xa5, 0xeb, 0xc4, 0xee, 0x36, 0x3b, 0x2a, 0xb6, 0x3b, 0x43, 0x0f, 0xa1, 0xd4, 0xc8, 0x97, 0x0b, 0x8f, 0x61, 0x81, - 0x70, 0x46, 0x98, 0x3e, 0x9e, 0x1f, 0x06, 0xcc, 0x5f, 0xa5, 0xe3, 0x90, 0xfb, 0xb3, 0x68, 0x8e, 0xab, 0x61, 0x44, - 0x03, 0x51, 0x3a, 0x42, 0xe8, 0x6a, 0xfb, 0x82, 0x18, 0xf3, 0x2b, 0x12, 0x70, 0x01, 0x21, 0x70, 0x66, 0x9f, 0x45, - 0xa3, 0x29, 0x1c, 0xf1, 0x0a, 0x71, 0x63, 0x75, 0x1c, 0x46, 0x39, 0x8b, 0x38, 0x7b, 0x96, 0x30, 0x7c, 0xc3, 0x1d, - 0x80, 0x9e, 0xb6, 0xeb, 0x15, 0xea, 0xdc, 0x25, 0x31, 0x7f, 0x9d, 0xc1, 0x3c, 0x3d, 0x41, 0x24, 0x40, 0xc5, 0xc5, - 0xd6, 0x56, 0x8c, 0x24, 0xb2, 0xcf, 0x61, 0xb7, 0x4e, 0x16, 0xc0, 0x04, 0xec, 0x14, 0x5b, 0xd8, 0x80, 0x6d, 0x2f, - 0xf6, 0x39, 0x20, 0xf1, 0x49, 0x96, 0x72, 0x18, 0x0e, 0xe0, 0xd5, 0x14, 0xe4, 0x47, 0xf3, 0x39, 0x4b, 0xc7, 0x4f, - 0xa6, 0x71, 0x32, 0x06, 0x6c, 0x94, 0xb0, 0xde, 0x8c, 0x85, 0xb0, 0x4e, 0x58, 0x4c, 0x70, 0xf3, 0x8a, 0x68, 0xfb, - 0x90, 0x90, 0x79, 0x68, 0xdb, 0x3d, 0xe4, 0x40, 0x72, 0x15, 0xc8, 0x83, 0x68, 0xe3, 0xde, 0x01, 0xeb, 0x2f, 0x5c, - 0xbe, 0x1d, 0xc6, 0x7a, 0x1b, 0x25, 0x82, 0x9f, 0x20, 0xa7, 0x01, 0xfc, 0x33, 0xe0, 0x81, 0x3d, 0x64, 0x5c, 0xdf, - 0x49, 0xae, 0x93, 0x32, 0xb5, 0x42, 0x40, 0xc0, 0x08, 0x39, 0x88, 0xc4, 0xc1, 0xdb, 0x2c, 0xb9, 0x9c, 0xc4, 0x49, - 0x72, 0xb0, 0x98, 0xcf, 0xb3, 0x9c, 0x7b, 0x5f, 0x87, 0x4b, 0x9e, 0x55, 0x6b, 0xa5, 0x43, 0x5e, 0x9c, 0xc7, 0x1c, - 0x11, 0xea, 0x2e, 0x47, 0x11, 0x6c, 0xf5, 0x97, 0x59, 0x96, 0xb0, 0x28, 0x85, 0x65, 0xb0, 0x81, 0x6d, 0x07, 0xe9, - 0x22, 0x49, 0x7a, 0x27, 0x30, 0xec, 0xa7, 0x1e, 0x55, 0x0b, 0x8e, 0x1f, 0xd0, 0xf3, 0x7e, 0x9e, 0x47, 0x97, 0xd0, - 0x10, 0xdb, 0x00, 0x2d, 0xc2, 0x6e, 0x7d, 0x7d, 0xf0, 0xe6, 0xb5, 0x2f, 0x08, 0x3f, 0x9e, 0x5c, 0x02, 0xa0, 0x65, - 0xc5, 0x35, 0x27, 0x79, 0x36, 0x6b, 0x4c, 0x8d, 0x78, 0x88, 0x43, 0xd6, 0xbb, 0x06, 0x84, 0x98, 0x46, 0x86, 0x5d, - 0x62, 0x26, 0x04, 0xaf, 0x89, 0x9e, 0x65, 0x25, 0x9e, 0x81, 0x01, 0x3e, 0x04, 0xa2, 0x18, 0xa6, 0xbc, 0x19, 0x5a, - 0x9e, 0x5f, 0x2e, 0xe3, 0x90, 0xe0, 0x9c, 0xa3, 0xfc, 0x45, 0x18, 0x47, 0x11, 0xcc, 0xbe, 0x14, 0x03, 0x96, 0x0a, - 0xe2, 0xb8, 0x2c, 0xbd, 0x44, 0x13, 0x31, 0x72, 0x3c, 0x64, 0x28, 0x1c, 0x8e, 0xd1, 0xd5, 0x15, 0x83, 0x17, 0xd7, - 0xfb, 0x26, 0x5c, 0x46, 0x6a, 0x3d, 0x28, 0xa1, 0xf0, 0x7c, 0x05, 0x82, 0x4f, 0xa0, 0x24, 0x3b, 0x03, 0x39, 0x08, - 0x70, 0x7e, 0xed, 0x81, 0xfc, 0x4f, 0x10, 0x8a, 0x8d, 0x8e, 0x07, 0x12, 0xf4, 0xc9, 0x34, 0x4a, 0x4f, 0xd9, 0x38, - 0x48, 0x58, 0x29, 0x39, 0xef, 0xbe, 0x05, 0x7b, 0x0c, 0xe4, 0x54, 0x58, 0xcf, 0x0f, 0x5f, 0xbd, 0x94, 0x3b, 0x57, - 0x63, 0xc6, 0xb0, 0x49, 0x0b, 0x10, 0xab, 0xc0, 0xb6, 0x25, 0x3b, 0x7e, 0xc6, 0x15, 0xf7, 0x16, 0x25, 0x71, 0xf1, - 0x7e, 0x0e, 0x2a, 0x06, 0x7b, 0x0b, 0xc3, 0xc0, 0xf4, 0x21, 0x4c, 0x45, 0xe5, 0x30, 0x9f, 0xa8, 0x18, 0xeb, 0x22, - 0xe8, 0x2c, 0x56, 0x2a, 0x5e, 0x33, 0xc7, 0x2d, 0x81, 0x54, 0x79, 0x3c, 0xb2, 0xa2, 0xf1, 0xf8, 0x45, 0x1a, 0xf3, - 0x38, 0x4a, 0xe2, 0xdf, 0x08, 0x93, 0x4b, 0xa4, 0x31, 0xde, 0x93, 0x9b, 0x00, 0x6b, 0xa7, 0x1e, 0x89, 0xab, 0x98, - 0xec, 0x06, 0x21, 0x43, 0x70, 0xcb, 0x24, 0x3c, 0x1a, 0x4a, 0xf0, 0x12, 0x7f, 0xbe, 0x28, 0xa6, 0x88, 0x58, 0x39, - 0x30, 0x32, 0xf2, 0xec, 0xa4, 0x60, 0xf9, 0x19, 0x1b, 0x6b, 0x0a, 0x28, 0x60, 0x55, 0xd4, 0x1c, 0x94, 0x17, 0x9a, - 0xd1, 0x51, 0x32, 0x94, 0xc1, 0x50, 0x3d, 0x93, 0xcd, 0x32, 0x49, 0xcc, 0x5a, 0xc3, 0xd1, 0x5c, 0xc0, 0x11, 0x4a, - 0x85, 0xe4, 0x04, 0x45, 0xa8, 0x56, 0x38, 0x05, 0x2e, 0x04, 0x52, 0xc1, 0x3c, 0xe6, 0x4a, 0x92, 0x3d, 0x5b, 0x90, - 0x48, 0x28, 0xa0, 0x23, 0x1c, 0x64, 0x82, 0xb4, 0x70, 0xe1, 0x54, 0x01, 0x97, 0x97, 0xe0, 0x0a, 0x2e, 0xa2, 0xd4, - 0x1c, 0x24, 0x80, 0xf0, 0x1b, 0x21, 0x0b, 0x7d, 0x6c, 0x41, 0x64, 0xe0, 0xeb, 0x9d, 0x07, 0xc4, 0xca, 0x75, 0x57, - 0x0b, 0xf1, 0xae, 0x01, 0x1b, 0x27, 0x46, 0x7a, 0xf2, 0x36, 0xb8, 0x9f, 0x66, 0xfb, 0xa3, 0x11, 0x2b, 0x8a, 0x2c, - 0xdf, 0xda, 0xda, 0xa0, 0xf6, 0xd7, 0x29, 0x5a, 0x80, 0x49, 0x57, 0xf3, 0x3a, 0xbb, 0x20, 0x09, 0x6e, 0x8a, 0x15, - 0x25, 0xd3, 0x03, 0xfb, 0xe3, 0x47, 0xe0, 0xd9, 0x9e, 0x44, 0x03, 0x60, 0x7d, 0x55, 0xf1, 0x13, 0xfa, 0x4c, 0x1d, - 0x33, 0x6b, 0xf5, 0x4b, 0xa7, 0x0e, 0x92, 0x07, 0xc3, 0xba, 0xa5, 0xb1, 0xa1, 0x6b, 0x87, 0xc6, 0xdd, 0x90, 0x02, - 0x72, 0x79, 0x4a, 0x22, 0xdb, 0xd8, 0x46, 0xd0, 0xda, 0x4a, 0x8f, 0x50, 0xaf, 0x56, 0x93, 0x13, 0xa0, 0x47, 0x6c, - 0xd8, 0x93, 0xf5, 0x61, 0x21, 0x30, 0x97, 0xb3, 0x5f, 0x17, 0xac, 0xe0, 0x82, 0x74, 0x61, 0xdc, 0x1c, 0xc6, 0x2d, - 0x57, 0xb4, 0xc3, 0x9a, 0xee, 0xb8, 0x0e, 0xb6, 0x37, 0x73, 0x94, 0x63, 0x05, 0x52, 0xf2, 0xcd, 0xe4, 0x84, 0xb0, - 0x32, 0xf7, 0xea, 0xea, 0x1b, 0x35, 0x48, 0xb5, 0x95, 0x5a, 0x07, 0x6a, 0xec, 0x89, 0xad, 0x9a, 0x8c, 0x6d, 0x57, - 0x0a, 0xd4, 0x8d, 0x4e, 0xaf, 0x46, 0x07, 0x70, 0xe6, 0xda, 0x9a, 0xa4, 0x2b, 0x65, 0xfb, 0xad, 0xc2, 0xe9, 0x1b, - 0x31, 0x32, 0x69, 0xa3, 0xec, 0x76, 0xea, 0x51, 0x27, 0x1e, 0xda, 0xae, 0xd4, 0x55, 0x8c, 0x61, 0x51, 0x67, 0x0c, - 0x4d, 0xa8, 0xe7, 0xba, 0x8b, 0xad, 0x89, 0x8a, 0x85, 0x6a, 0xaf, 0x95, 0x01, 0xc1, 0xc3, 0x23, 0x50, 0x4e, 0xd6, - 0xda, 0x07, 0xaf, 0xa3, 0x19, 0x43, 0x8c, 0x7a, 0xd7, 0x35, 0x90, 0x06, 0x04, 0x34, 0x19, 0x36, 0xc5, 0x1b, 0x77, - 0x85, 0xd6, 0x54, 0x3f, 0x5f, 0x31, 0x68, 0x11, 0xa0, 0x5f, 0x97, 0x6b, 0xb6, 0x88, 0xe4, 0xa6, 0x24, 0x67, 0x85, - 0x1f, 0x51, 0x26, 0xf6, 0x84, 0x04, 0x3c, 0x2c, 0x1e, 0xb6, 0xbf, 0xb1, 0x71, 0xb2, 0x15, 0x53, 0x6b, 0xe4, 0xc8, - 0x53, 0x00, 0xcf, 0x24, 0x04, 0x80, 0x5d, 0xd2, 0xcf, 0xda, 0xc1, 0x42, 0xb4, 0x1d, 0x20, 0x1d, 0xf8, 0x93, 0x24, - 0xe2, 0x4e, 0x67, 0xa7, 0xed, 0x02, 0x1d, 0x02, 0x13, 0x07, 0x19, 0x01, 0xea, 0x7d, 0xb5, 0x14, 0x86, 0x4b, 0x89, - 0x5d, 0xee, 0x83, 0x52, 0x34, 0x8d, 0x27, 0xdc, 0xc9, 0x50, 0x88, 0xb8, 0x25, 0x4b, 0x40, 0xc8, 0xe8, 0x73, 0x05, - 0x5c, 0x82, 0x0b, 0xee, 0x22, 0xaa, 0x35, 0x43, 0x53, 0x90, 0x12, 0x97, 0x22, 0x29, 0xa8, 0x20, 0x30, 0x98, 0x4a, - 0x4f, 0x51, 0x14, 0xc8, 0xb7, 0x78, 0x20, 0x06, 0x0d, 0x56, 0x34, 0xca, 0x78, 0x10, 0xaf, 0x16, 0x82, 0x18, 0xf6, - 0x79, 0xf6, 0x32, 0x3b, 0x67, 0xf9, 0x93, 0x08, 0x61, 0x0f, 0x44, 0xf7, 0x12, 0x38, 0x3d, 0x31, 0x74, 0xd6, 0x53, - 0xb4, 0x72, 0x46, 0x8b, 0x86, 0x8d, 0x98, 0xc5, 0x28, 0x08, 0x41, 0xca, 0x11, 0xee, 0x53, 0x3c, 0x52, 0x74, 0xf6, - 0x50, 0x94, 0x30, 0x4d, 0x5b, 0xfb, 0x2f, 0xeb, 0xb4, 0x05, 0x23, 0xcc, 0x15, 0xb5, 0xd6, 0x4f, 0xac, 0xeb, 0x49, - 0xd9, 0xec, 0x48, 0xda, 0x32, 0x84, 0x19, 0xc8, 0x8f, 0xab, 0xab, 0x4a, 0x49, 0x07, 0x61, 0xaa, 0xb9, 0x39, 0x6a, - 0x4e, 0xe2, 0x48, 0xb8, 0x25, 0x08, 0x23, 0x54, 0xbc, 0xf2, 0x2c, 0x49, 0x0c, 0x59, 0xe4, 0xc5, 0x3d, 0xa7, 0x21, - 0x8e, 0x00, 0x8a, 0x59, 0x4d, 0x22, 0x0d, 0x78, 0xa0, 0x2b, 0x50, 0x28, 0x29, 0x69, 0xe4, 0x55, 0x4d, 0x04, 0xc4, - 0xe9, 0x98, 0xe5, 0xc2, 0x40, 0x93, 0x32, 0x14, 0x26, 0x4c, 0x81, 0xa0, 0xd9, 0x18, 0x38, 0xbc, 0x5a, 0x00, 0xa8, - 0x27, 0xfe, 0x34, 0x2b, 0xb8, 0xae, 0x33, 0xa1, 0x8f, 0xaf, 0xae, 0x62, 0x61, 0x2f, 0x22, 0x01, 0xe4, 0x6c, 0x96, - 0x9d, 0xb1, 0x35, 0x50, 0xf7, 0xd4, 0x60, 0x26, 0xc8, 0xc6, 0x30, 0x20, 0x44, 0x41, 0xb4, 0xcc, 0x93, 0x78, 0xc4, - 0xb4, 0x94, 0x9a, 0xf9, 0xa0, 0xd0, 0xb1, 0x0b, 0xe0, 0x11, 0xcc, 0xed, 0xf7, 0xfb, 0x6d, 0xaf, 0xe3, 0x96, 0x02, - 0xe1, 0xcb, 0x15, 0x8c, 0xde, 0x20, 0x1f, 0xa5, 0x0a, 0xbe, 0x8e, 0x17, 0x70, 0xd7, 0x10, 0x8a, 0x5c, 0xd8, 0x49, - 0x9e, 0x64, 0xc4, 0xae, 0x37, 0x86, 0x41, 0x39, 0x53, 0x8c, 0x1b, 0x55, 0x5c, 0x71, 0x6c, 0xdf, 0x69, 0xb4, 0x69, - 0x72, 0x52, 0x27, 0x4c, 0x6d, 0x8c, 0xdc, 0xf3, 0x42, 0x5b, 0xc0, 0xe6, 0xf6, 0xa0, 0x96, 0x48, 0xd5, 0x40, 0xeb, - 0x00, 0xa1, 0xb0, 0x74, 0x9d, 0x95, 0x25, 0x55, 0x9d, 0x25, 0x13, 0xd7, 0x07, 0xe8, 0x0d, 0x93, 0x60, 0xae, 0x43, - 0xc1, 0x81, 0x64, 0x08, 0x1c, 0x2d, 0x32, 0xb1, 0x5f, 0x4f, 0x60, 0x7b, 0x4e, 0xa2, 0xd1, 0x27, 0x0d, 0x6e, 0x85, - 0xf6, 0x26, 0x19, 0x38, 0x8d, 0x92, 0xd0, 0x60, 0x57, 0xe6, 0xba, 0x15, 0x87, 0xae, 0x1d, 0x14, 0x30, 0xc8, 0x56, - 0xc8, 0xbe, 0xb9, 0xd1, 0x4d, 0x6a, 0x97, 0xe4, 0xa1, 0xec, 0x27, 0x4d, 0x25, 0x37, 0x90, 0x1c, 0x57, 0xdc, 0x80, - 0x2b, 0xc2, 0x83, 0xad, 0x69, 0x40, 0x02, 0x74, 0x57, 0x8e, 0xe3, 0xe2, 0x7a, 0x14, 0xfc, 0xa9, 0x60, 0x3e, 0x35, - 0x66, 0xba, 0x15, 0x52, 0xcd, 0xe1, 0xa4, 0x1a, 0xac, 0x41, 0x93, 0xca, 0x83, 0x62, 0x35, 0xdf, 0xa0, 0xa2, 0x42, - 0x14, 0x7f, 0x2a, 0xaa, 0x50, 0x05, 0x43, 0x30, 0x0a, 0x2f, 0x97, 0x04, 0x97, 0xad, 0xb2, 0x16, 0xc9, 0x53, 0x63, - 0x12, 0xa9, 0x9a, 0xe4, 0x32, 0x50, 0xb0, 0xe8, 0xb4, 0xfa, 0x52, 0x13, 0x57, 0x2c, 0x37, 0x0d, 0x35, 0x33, 0xc9, - 0x95, 0x35, 0xe1, 0x14, 0x68, 0x77, 0x29, 0xed, 0xdd, 0x5c, 0x4f, 0xa1, 0xd6, 0x53, 0xf8, 0x86, 0x0d, 0x65, 0xd2, - 0x76, 0x3e, 0x00, 0x75, 0xbf, 0x56, 0x89, 0xfa, 0xa9, 0x8f, 0x8c, 0xd9, 0xd5, 0x4c, 0x17, 0x18, 0x8a, 0x24, 0x93, - 0x74, 0x20, 0xe9, 0x0d, 0xd9, 0x46, 0x65, 0x19, 0x65, 0xae, 0x38, 0x20, 0x35, 0xab, 0x34, 0xf3, 0x52, 0xb7, 0xa1, - 0xbf, 0x97, 0xa5, 0xc4, 0x13, 0x17, 0x98, 0x89, 0xbd, 0x9b, 0x70, 0xe3, 0xa5, 0x61, 0x26, 0xb4, 0x5f, 0xa1, 0xec, - 0xd4, 0x30, 0x94, 0x4a, 0x16, 0x88, 0x63, 0xe3, 0x6b, 0xa5, 0x19, 0x64, 0xfe, 0x1a, 0x7d, 0x0a, 0x40, 0x49, 0x60, - 0xf3, 0x35, 0x96, 0xbc, 0x28, 0xac, 0xe3, 0x71, 0x83, 0xf0, 0x58, 0xb1, 0xd0, 0x1a, 0xcb, 0xd7, 0xf2, 0x2c, 0xf6, - 0x6b, 0x26, 0xa1, 0x89, 0xc9, 0x62, 0x50, 0x04, 0xb6, 0x72, 0x44, 0x54, 0xb2, 0x2d, 0x19, 0x24, 0x64, 0x90, 0xae, - 0x22, 0xbd, 0x36, 0x92, 0x81, 0xeb, 0x54, 0x70, 0xb4, 0x74, 0x18, 0x46, 0x0e, 0x1a, 0xee, 0xb4, 0x17, 0x2b, 0x88, - 0x6c, 0xea, 0x9b, 0x44, 0x8a, 0x68, 0x9c, 0x16, 0xa8, 0xc2, 0x99, 0x32, 0xdd, 0x71, 0x60, 0x39, 0xc0, 0xf6, 0x57, - 0x48, 0x6f, 0xad, 0xda, 0xe9, 0xfa, 0x95, 0xc1, 0x77, 0x75, 0x95, 0x20, 0x3d, 0x08, 0x85, 0x17, 0xf6, 0x6c, 0xa0, - 0x78, 0xef, 0xfe, 0x4b, 0x6c, 0x45, 0xfa, 0x67, 0x55, 0x52, 0x59, 0x0a, 0x35, 0xca, 0xad, 0xef, 0x13, 0x33, 0x5d, - 0x8b, 0xaa, 0xe2, 0xc0, 0xe0, 0xea, 0x07, 0x4a, 0x60, 0x57, 0x4b, 0x3e, 0x90, 0x43, 0xc7, 0xae, 0xeb, 0x06, 0x05, - 0x19, 0x2f, 0x1b, 0xeb, 0x4c, 0xc8, 0xad, 0x2d, 0xd3, 0x66, 0x3a, 0xd3, 0xc3, 0x3f, 0x71, 0x50, 0x38, 0x17, 0x97, - 0x29, 0x69, 0x30, 0x4f, 0x94, 0x38, 0x5a, 0x31, 0x40, 0xdb, 0x3d, 0xb4, 0xb4, 0xa3, 0xf3, 0x28, 0xe6, 0x96, 0x1e, - 0x45, 0x58, 0xda, 0xc8, 0x9f, 0xa4, 0xd2, 0x01, 0xeb, 0x42, 0x15, 0x92, 0x8c, 0x70, 0x53, 0x17, 0x2d, 0x46, 0x53, - 0x86, 0x2e, 0x70, 0xa5, 0x4f, 0x98, 0xbc, 0x67, 0x03, 0xd7, 0x2d, 0x06, 0x66, 0xeb, 0x61, 0x2f, 0x9b, 0xdd, 0x6b, - 0xea, 0x3f, 0xec, 0x11, 0xf0, 0xb6, 0x99, 0xaa, 0x2b, 0x1b, 0xef, 0x92, 0x45, 0xa2, 0x87, 0x6d, 0xdd, 0xd8, 0x52, - 0xd7, 0xef, 0x35, 0xcc, 0xeb, 0xca, 0x30, 0xaf, 0x09, 0xd5, 0x86, 0x1c, 0x56, 0x66, 0x0e, 0x33, 0x0d, 0x79, 0xb1, - 0x83, 0x6e, 0x4f, 0x38, 0x85, 0xc0, 0x88, 0xd0, 0xfa, 0xa0, 0xa2, 0x06, 0x42, 0x25, 0x57, 0x52, 0x35, 0x5b, 0x24, - 0x63, 0x09, 0x2c, 0x98, 0xb0, 0x5c, 0xd2, 0xd1, 0x79, 0x9c, 0x24, 0x55, 0xe9, 0x9f, 0xca, 0xe0, 0xc5, 0xb0, 0xb7, - 0xb1, 0x76, 0xb1, 0xa2, 0x85, 0x02, 0xc1, 0xd5, 0x4a, 0xd8, 0x7b, 0xc7, 0xad, 0xf6, 0x5d, 0x78, 0x1c, 0xb9, 0xe9, - 0x8d, 0x80, 0x7a, 0xf4, 0xb0, 0x6a, 0xd2, 0xde, 0x7f, 0x86, 0x2e, 0x35, 0x63, 0x3d, 0x28, 0xce, 0xa8, 0xf8, 0x77, - 0xe9, 0x53, 0xbf, 0x73, 0x79, 0xb7, 0x8a, 0xae, 0xa6, 0x43, 0x45, 0x39, 0x3e, 0x4c, 0x17, 0x4b, 0x5b, 0x39, 0x02, - 0x72, 0x3d, 0x2c, 0x72, 0x01, 0x13, 0x35, 0x58, 0x50, 0x8a, 0x55, 0x6b, 0x61, 0xf7, 0xf2, 0x36, 0x67, 0x0e, 0xb9, - 0xc2, 0x45, 0xff, 0x27, 0xd9, 0x6c, 0x8e, 0x9a, 0x59, 0x83, 0xa8, 0xa1, 0xc1, 0xfb, 0x46, 0x7d, 0xb9, 0xa6, 0xac, - 0xd6, 0x87, 0x4e, 0x64, 0x8d, 0x9e, 0xb4, 0xa1, 0x0c, 0x06, 0xd5, 0x42, 0x17, 0xd5, 0xf5, 0xe6, 0x26, 0x8b, 0x59, - 0x47, 0xe3, 0x3e, 0xc9, 0x6d, 0xad, 0x4d, 0x7a, 0x1a, 0x07, 0xc4, 0x93, 0x24, 0xc1, 0x9b, 0x04, 0x50, 0x56, 0xc8, - 0x59, 0x96, 0x0d, 0xf4, 0x2d, 0xcb, 0x12, 0xf7, 0xef, 0xdb, 0xde, 0x7e, 0xcd, 0xb2, 0xf6, 0xf6, 0xaf, 0x37, 0x91, - 0xab, 0x3a, 0x69, 0x41, 0x1e, 0x0d, 0xa1, 0x68, 0x45, 0xa7, 0x0c, 0x97, 0xb3, 0x6c, 0xcc, 0x02, 0x1b, 0xba, 0xa7, - 0x76, 0xa9, 0xa4, 0x32, 0x1c, 0x8e, 0x94, 0x39, 0xcb, 0x77, 0x75, 0x4f, 0x6a, 0xb0, 0x0f, 0x24, 0xa0, 0xd5, 0x85, - 0xef, 0xc2, 0xd3, 0x24, 0x3b, 0x89, 0x92, 0x43, 0x21, 0xc0, 0x6b, 0x2d, 0x3f, 0x80, 0xc9, 0x48, 0x1a, 0xab, 0x21, - 0xa4, 0xbe, 0x1b, 0x7c, 0x17, 0xdc, 0xde, 0xa3, 0xb2, 0x56, 0xec, 0x8e, 0xdf, 0xf6, 0x3b, 0xb6, 0xf2, 0x88, 0xbd, - 0x34, 0xa7, 0x03, 0x89, 0x53, 0x00, 0x66, 0x0e, 0x41, 0x92, 0x15, 0x5e, 0xc4, 0xc2, 0x97, 0x83, 0x97, 0xca, 0xa4, - 0xce, 0xc0, 0x84, 0x00, 0x23, 0x3f, 0x89, 0x79, 0x0b, 0xe3, 0x91, 0xb6, 0xb7, 0x14, 0x15, 0xe8, 0x57, 0x24, 0xbf, - 0x74, 0xa9, 0xac, 0x41, 0xef, 0x63, 0x78, 0x0c, 0xcd, 0x36, 0x37, 0x97, 0xce, 0xab, 0x88, 0x4f, 0xfd, 0x3c, 0x4a, - 0xc7, 0xd9, 0xcc, 0x71, 0xb7, 0x6d, 0xdb, 0xf5, 0x0b, 0xb2, 0x44, 0xbe, 0x70, 0xcb, 0xcd, 0x63, 0x6f, 0xca, 0x42, - 0x7b, 0x60, 0x6f, 0x7f, 0xf4, 0xde, 0xb2, 0xf0, 0x78, 0x6f, 0x73, 0x39, 0x65, 0x65, 0xff, 0xd8, 0xbb, 0xd0, 0x3e, - 0x77, 0xef, 0x2d, 0x72, 0x19, 0xe8, 0x15, 0xf6, 0x2f, 0x24, 0x18, 0x40, 0x6e, 0xe4, 0x7f, 0x07, 0x2e, 0xf7, 0x9e, - 0x02, 0x22, 0xd2, 0x4f, 0x7b, 0x75, 0x65, 0x67, 0xe4, 0x31, 0xb0, 0x37, 0xb4, 0xb1, 0xba, 0xb5, 0x55, 0x89, 0xf9, - 0xaa, 0xd4, 0x1b, 0xb1, 0xb0, 0x66, 0xa9, 0x7b, 0xef, 0x29, 0xb4, 0x52, 0x3f, 0xc8, 0x23, 0x46, 0x42, 0x73, 0x55, - 0x4f, 0x70, 0x8c, 0x23, 0xbe, 0xfe, 0x58, 0x1f, 0x09, 0x2f, 0x85, 0x1f, 0x83, 0xf6, 0x12, 0x81, 0xf8, 0x06, 0x03, - 0xc7, 0x3b, 0x0c, 0x77, 0xf6, 0x9c, 0x41, 0xe0, 0x6c, 0xb4, 0x5a, 0x57, 0x3f, 0xed, 0x1c, 0xfd, 0x1c, 0xb5, 0x7e, - 0xdb, 0x6f, 0xfd, 0x38, 0x74, 0xaf, 0x9c, 0x9f, 0x76, 0x06, 0x47, 0xf2, 0xed, 0xe8, 0xe7, 0xfe, 0x4f, 0xc5, 0xf0, - 0x73, 0x51, 0xb8, 0xe9, 0xba, 0x3b, 0xa7, 0x60, 0x29, 0x85, 0x3b, 0xad, 0x56, 0x1f, 0x9e, 0x16, 0xf0, 0x84, 0x3f, - 0x2f, 0xe1, 0xc7, 0xd5, 0x91, 0xf5, 0x1f, 0x7e, 0x4a, 0xff, 0xe3, 0x4f, 0xf9, 0x10, 0xc7, 0x3c, 0xfa, 0xf9, 0xa7, - 0xc2, 0xbe, 0xd7, 0x0f, 0x77, 0x86, 0xdb, 0xae, 0xa3, 0x6b, 0x3e, 0x0f, 0xab, 0x47, 0x68, 0x75, 0xf4, 0xb3, 0x7c, - 0xb3, 0xef, 0x1d, 0xef, 0xf5, 0xc3, 0xe1, 0x95, 0x63, 0x5f, 0xdd, 0x73, 0xaf, 0x5c, 0xf7, 0x6a, 0x13, 0xe7, 0x99, - 0xc3, 0xe8, 0xf7, 0xe0, 0xe7, 0x19, 0xfc, 0xb4, 0xe1, 0xe7, 0x29, 0xfc, 0xfc, 0x19, 0xba, 0x09, 0xff, 0xdb, 0x15, - 0xf9, 0x42, 0xae, 0x30, 0x60, 0x11, 0xc1, 0x2e, 0xb8, 0x9b, 0x3b, 0xb1, 0x37, 0x21, 0xa4, 0xc1, 0x39, 0xf4, 0x7d, - 0x1f, 0xdd, 0xa4, 0xce, 0xf2, 0xe3, 0x26, 0x6c, 0x3a, 0x52, 0xce, 0x66, 0xc0, 0x3c, 0xe1, 0x39, 0x28, 0x02, 0x2e, - 0x62, 0xab, 0x05, 0x06, 0x57, 0xbd, 0x45, 0x38, 0x61, 0x0e, 0x28, 0x05, 0x87, 0x0c, 0x1f, 0xba, 0xae, 0xf7, 0x4c, - 0xc6, 0x0c, 0xf1, 0x9c, 0x0b, 0xd2, 0x4a, 0x33, 0xa1, 0xd2, 0xd8, 0xae, 0x37, 0x5f, 0x53, 0x09, 0xc7, 0x3a, 0x3d, - 0x85, 0xba, 0x4d, 0x11, 0x68, 0xfb, 0x8e, 0x45, 0x9f, 0xf0, 0x48, 0x3e, 0x37, 0x82, 0xc0, 0x2b, 0x9a, 0x7c, 0x53, - 0x69, 0x34, 0x74, 0x44, 0x61, 0x8e, 0x7d, 0xc9, 0x60, 0x86, 0x15, 0x15, 0x91, 0x93, 0xd0, 0x14, 0x9a, 0x2d, 0x4c, - 0xfe, 0x36, 0xca, 0xf9, 0x66, 0xa5, 0xd8, 0x86, 0x35, 0x4d, 0xb6, 0xa9, 0xe9, 0xdf, 0x61, 0x0a, 0x54, 0x2d, 0x29, - 0xfe, 0x61, 0x8e, 0x1f, 0xa6, 0xb4, 0xac, 0xd7, 0x0e, 0x07, 0x0b, 0xbd, 0x00, 0xbe, 0x23, 0xfa, 0x39, 0x6f, 0x51, - 0x8c, 0xc1, 0x5f, 0xe9, 0x66, 0xf0, 0xc4, 0x7c, 0xe8, 0xa2, 0x59, 0x96, 0xda, 0xb9, 0x95, 0x22, 0xbb, 0x7f, 0x81, - 0x27, 0x23, 0x2d, 0xbd, 0x83, 0x50, 0x9d, 0x98, 0xc3, 0x9c, 0xb1, 0xef, 0xa2, 0xe4, 0x13, 0xcb, 0x9d, 0x0b, 0xaf, - 0xd3, 0xfd, 0x82, 0x3a, 0x7b, 0xa8, 0x9b, 0xbd, 0xae, 0xc2, 0x68, 0x4a, 0x2d, 0x50, 0x21, 0xc2, 0x56, 0xc7, 0x43, - 0x8e, 0x41, 0x28, 0xc8, 0xbd, 0x2c, 0xec, 0x12, 0x85, 0xdb, 0x7b, 0xc5, 0xd9, 0x69, 0xdf, 0x0e, 0x6c, 0x1b, 0x34, - 0xfe, 0x43, 0x72, 0x5b, 0x09, 0xc5, 0x02, 0x14, 0xb2, 0xbd, 0xb8, 0xc7, 0xb7, 0xb7, 0x2b, 0x87, 0x13, 0x06, 0xd2, - 0xa9, 0x7b, 0xe2, 0x45, 0xde, 0x34, 0x84, 0x01, 0x47, 0xd0, 0x0c, 0xbb, 0xf4, 0x46, 0x7b, 0xb1, 0x9c, 0x06, 0x7d, - 0x21, 0x7e, 0x12, 0x15, 0xfc, 0x05, 0xfa, 0x23, 0xc2, 0x11, 0x2a, 0xfb, 0x3e, 0xbb, 0x60, 0x23, 0xa5, 0x67, 0x00, - 0xa2, 0x22, 0xb7, 0xe7, 0x8e, 0x42, 0xa3, 0x19, 0xcc, 0x1d, 0x86, 0x87, 0x03, 0x1b, 0xce, 0x12, 0x9c, 0xca, 0x30, - 0x3a, 0xea, 0x0c, 0x07, 0x69, 0x08, 0xbc, 0x56, 0xe3, 0x56, 0x16, 0x2d, 0x6a, 0x45, 0xdd, 0xe1, 0xc0, 0x39, 0x05, - 0x25, 0x1d, 0x74, 0x71, 0x07, 0xdf, 0xd0, 0x43, 0x91, 0x87, 0xef, 0xd8, 0xe9, 0xb3, 0x8b, 0xb9, 0x63, 0xef, 0xed, - 0xd8, 0xdb, 0x58, 0xea, 0xd9, 0x40, 0x5e, 0x30, 0x77, 0x78, 0xe9, 0x9a, 0x9d, 0x77, 0x87, 0x08, 0x2a, 0x16, 0xe2, - 0xe4, 0x97, 0x03, 0xbb, 0x2f, 0xa6, 0x6e, 0xc3, 0xa0, 0xa9, 0xdc, 0x7e, 0xdc, 0xd1, 0x43, 0x5a, 0xaa, 0xea, 0xaa, - 0xa0, 0x83, 0xb2, 0x6e, 0xe0, 0x4c, 0xcd, 0x45, 0xb4, 0x70, 0x32, 0x89, 0x05, 0x30, 0x78, 0xb0, 0x19, 0x4c, 0x6a, - 0x74, 0xdb, 0x1d, 0x0e, 0x2e, 0x83, 0x7b, 0xf6, 0x3d, 0xf5, 0x72, 0xc6, 0x02, 0xb0, 0x2e, 0x68, 0xfa, 0x33, 0x94, - 0x22, 0xf0, 0x73, 0xce, 0x60, 0x91, 0x97, 0x54, 0x34, 0x96, 0x45, 0x0b, 0x2c, 0x3a, 0x0c, 0x10, 0x54, 0x2f, 0xd7, - 0xda, 0x9f, 0xd8, 0x93, 0x71, 0x48, 0xb0, 0x6f, 0x6d, 0xc1, 0xd6, 0x6c, 0x77, 0x86, 0x18, 0x6f, 0xc8, 0x79, 0xf1, - 0x5d, 0xcc, 0x41, 0x24, 0xec, 0xf4, 0x6d, 0x77, 0x60, 0x5b, 0xb8, 0xb5, 0xbd, 0x6c, 0x3b, 0x14, 0x18, 0x8e, 0xb7, - 0xdf, 0xb2, 0x60, 0xda, 0x0f, 0xdb, 0x03, 0xa7, 0x10, 0xa2, 0x23, 0xc1, 0xb8, 0xa5, 0xe0, 0xe0, 0x6d, 0x6f, 0x0a, - 0x0c, 0x1d, 0x29, 0x77, 0xd3, 0xde, 0x56, 0x85, 0x50, 0xf4, 0x71, 0x7b, 0xec, 0x06, 0x31, 0xfc, 0x70, 0x5a, 0x48, - 0x34, 0x53, 0xdd, 0x57, 0x4b, 0x66, 0x37, 0x18, 0x2b, 0x8d, 0x3c, 0x09, 0xb3, 0x6d, 0x07, 0x3d, 0xb4, 0xc0, 0x69, - 0xf7, 0x06, 0x00, 0xc3, 0xb6, 0xa3, 0x28, 0x6d, 0x47, 0x91, 0x9a, 0xd2, 0xcf, 0x8f, 0xaa, 0xed, 0x60, 0x83, 0x88, - 0xf9, 0x95, 0xf4, 0x01, 0xb0, 0x82, 0xc4, 0x2b, 0x86, 0x2a, 0xe6, 0xf5, 0xbc, 0x16, 0xdf, 0x5a, 0x2a, 0x56, 0xc4, - 0x3c, 0x83, 0x43, 0xf1, 0x52, 0x9b, 0x61, 0x42, 0xdd, 0x9e, 0x23, 0x32, 0x34, 0xc9, 0x87, 0x6d, 0x20, 0x7a, 0xe5, - 0x60, 0x4f, 0xcd, 0x63, 0x91, 0x84, 0x55, 0x73, 0xef, 0x08, 0x48, 0x7b, 0x18, 0xbe, 0x16, 0x11, 0xc7, 0x9e, 0xf2, - 0xe6, 0xb3, 0x24, 0x7c, 0xde, 0x08, 0x17, 0x47, 0x18, 0x11, 0x3a, 0xf0, 0x47, 0x8b, 0x1c, 0xf8, 0x01, 0x7f, 0x0d, - 0x9a, 0x41, 0x28, 0x9b, 0xa2, 0xa1, 0x87, 0x21, 0x60, 0x8f, 0x16, 0xde, 0x70, 0x9b, 0x1b, 0xd5, 0xa8, 0x51, 0x92, - 0xf2, 0x42, 0x81, 0xe1, 0x1e, 0x97, 0xa6, 0x3d, 0x32, 0x06, 0x19, 0x31, 0x76, 0x30, 0xe6, 0xef, 0x8f, 0xb0, 0x1a, - 0x27, 0x28, 0xdc, 0x92, 0x4e, 0x5b, 0xc5, 0xfe, 0x0e, 0xfc, 0x14, 0x38, 0x38, 0xd6, 0x81, 0x9d, 0xb5, 0xb5, 0x95, - 0xc8, 0x45, 0xed, 0xa5, 0x3d, 0x8a, 0x44, 0xa0, 0x3f, 0xb8, 0xf0, 0x53, 0xa8, 0x46, 0x14, 0x51, 0x11, 0x69, 0xa0, - 0x66, 0x54, 0xad, 0x82, 0xef, 0xc8, 0xf4, 0xc0, 0x73, 0x74, 0x5b, 0x93, 0xa2, 0xa8, 0x1b, 0x0b, 0x5f, 0xbe, 0xeb, - 0x52, 0x68, 0x0b, 0x03, 0x90, 0x82, 0xd0, 0x04, 0xc1, 0xb8, 0xe4, 0x94, 0xac, 0xe8, 0xef, 0xa3, 0xe1, 0x2b, 0x9f, - 0x1e, 0x65, 0xdb, 0xdb, 0x43, 0x11, 0xb7, 0x20, 0xc2, 0xe1, 0x86, 0x77, 0x35, 0xae, 0x00, 0xa8, 0x4f, 0xe7, 0xc4, - 0x75, 0xc7, 0xb4, 0x22, 0x4d, 0x97, 0x7c, 0x9f, 0x1c, 0x66, 0x00, 0x0c, 0xee, 0x38, 0x47, 0xfe, 0xe0, 0x2f, 0x43, - 0x30, 0x8f, 0xfd, 0xcf, 0xdd, 0x1d, 0xc5, 0x68, 0x7a, 0x32, 0xa6, 0xb8, 0xa4, 0x18, 0x6b, 0xc7, 0x23, 0xdf, 0x68, - 0x90, 0x7b, 0x29, 0xac, 0x00, 0xa4, 0x39, 0xf0, 0x84, 0x8a, 0x82, 0x90, 0xa2, 0x02, 0xdb, 0xc7, 0xc3, 0xcf, 0xf1, - 0x64, 0xbf, 0x03, 0x0d, 0x6f, 0xa0, 0xdf, 0x9e, 0xc2, 0xdb, 0x5f, 0xf4, 0xdb, 0x97, 0x2c, 0xf8, 0xa5, 0x94, 0xae, - 0xfb, 0xda, 0x14, 0x0f, 0xd5, 0x14, 0xa5, 0xd8, 0x22, 0x03, 0x87, 0xcc, 0x5d, 0xf5, 0xd9, 0x70, 0xb7, 0x04, 0x64, - 0x28, 0xd6, 0x05, 0x3a, 0x5a, 0x74, 0x8a, 0xc8, 0x75, 0x4d, 0x54, 0x18, 0xb9, 0x04, 0xe6, 0x82, 0x2b, 0xba, 0x25, - 0xe2, 0xec, 0xb7, 0xdd, 0x65, 0xad, 0x2d, 0xe9, 0x77, 0x6c, 0x36, 0xe7, 0x97, 0x07, 0x24, 0xe8, 0x03, 0x99, 0x36, - 0x20, 0x62, 0xe7, 0xed, 0x5e, 0xbc, 0xc7, 0x7b, 0x31, 0x70, 0xf5, 0x42, 0x91, 0x18, 0x9e, 0x55, 0xef, 0x2d, 0x7a, - 0x29, 0x4d, 0x62, 0xf2, 0x6a, 0xcb, 0xeb, 0xca, 0xe5, 0x6d, 0x6f, 0xc3, 0x02, 0x7b, 0x46, 0x57, 0x2e, 0xba, 0x96, - 0xa5, 0xc0, 0x09, 0x40, 0xf4, 0xb8, 0x4e, 0x72, 0x44, 0x71, 0x98, 0xcd, 0x86, 0x8c, 0x83, 0xb9, 0x6b, 0x47, 0xc5, - 0x31, 0xb1, 0xbb, 0x4c, 0xd8, 0x81, 0x95, 0x11, 0x95, 0xb7, 0x3a, 0xc2, 0x3b, 0x2c, 0xfa, 0x6b, 0xff, 0xf6, 0x47, - 0x8f, 0x6d, 0x77, 0x5c, 0x90, 0x20, 0xb5, 0xb1, 0x1e, 0x55, 0x63, 0x41, 0x7d, 0xf8, 0x51, 0x63, 0xa9, 0xcc, 0xb7, - 0xb7, 0xcb, 0x7a, 0xa8, 0x56, 0x9d, 0xe0, 0x5a, 0x34, 0xe5, 0xa2, 0x99, 0x0d, 0xc2, 0x01, 0x89, 0x09, 0x14, 0x68, - 0x6e, 0x65, 0xc5, 0x00, 0x43, 0xca, 0x72, 0xe4, 0x4f, 0x21, 0xf3, 0xe2, 0xb2, 0xd4, 0xa9, 0x2f, 0xd2, 0x1f, 0x19, - 0x62, 0xd4, 0x93, 0x94, 0x15, 0x10, 0xb0, 0x5e, 0xea, 0x25, 0xb4, 0x45, 0xb0, 0xf2, 0x67, 0x2a, 0x87, 0x46, 0x68, - 0x20, 0x51, 0x68, 0xa8, 0x25, 0x4a, 0xf9, 0xcc, 0xc3, 0x18, 0xa4, 0xfd, 0x93, 0x9a, 0xef, 0x2b, 0x57, 0x4a, 0x47, - 0x7e, 0x54, 0x0c, 0x03, 0xaa, 0x5f, 0x48, 0x0e, 0x36, 0x0d, 0xdf, 0x03, 0x19, 0x55, 0x86, 0x27, 0x31, 0xc2, 0xa7, - 0x71, 0xce, 0xc8, 0x52, 0xd8, 0x94, 0x30, 0x4b, 0xd5, 0x36, 0x52, 0xed, 0x22, 0xd3, 0x09, 0xe5, 0xc2, 0xfc, 0x53, - 0x23, 0x76, 0x91, 0x85, 0x2b, 0xad, 0x41, 0xfd, 0x78, 0x63, 0x02, 0x94, 0x5d, 0x5d, 0x65, 0xc2, 0xc6, 0x8d, 0x48, - 0xdf, 0xd0, 0x15, 0xd3, 0x81, 0x5a, 0x54, 0xe0, 0x44, 0xa4, 0xf1, 0x50, 0x0c, 0x85, 0x46, 0x38, 0xa4, 0x28, 0x72, - 0xe1, 0x1a, 0x87, 0xbe, 0x18, 0x68, 0xdb, 0x28, 0x0d, 0x9d, 0x04, 0x98, 0x80, 0x58, 0xbb, 0xa1, 0x4d, 0xa5, 0x83, - 0x34, 0x48, 0xa8, 0x14, 0xed, 0x1c, 0x58, 0x7f, 0x18, 0x49, 0x0c, 0x80, 0xfe, 0x50, 0x8d, 0x14, 0x51, 0x96, 0x05, - 0x6e, 0x00, 0xcd, 0x75, 0x80, 0x3b, 0xe1, 0x0b, 0x05, 0x15, 0xa6, 0xa7, 0x59, 0x79, 0x29, 0x84, 0xc8, 0xab, 0x35, - 0x29, 0x6b, 0xc4, 0x93, 0xcf, 0xd0, 0xe0, 0x53, 0xd6, 0xf5, 0x6b, 0xb9, 0x0e, 0x5d, 0xf0, 0x14, 0xb6, 0x55, 0x3d, - 0xbf, 0x0a, 0x39, 0x19, 0xd7, 0x20, 0x2b, 0x24, 0xd3, 0x5f, 0x31, 0x92, 0xf7, 0x5f, 0xf9, 0x55, 0x2d, 0x35, 0x86, - 0xb2, 0xf7, 0xeb, 0x9a, 0x61, 0x79, 0x39, 0xaf, 0xdc, 0x14, 0x04, 0xdc, 0x92, 0x25, 0xc1, 0x52, 0x4a, 0x08, 0xd0, - 0xb0, 0x3d, 0x92, 0x4a, 0x41, 0x51, 0x6a, 0xf7, 0xce, 0x53, 0xd0, 0x02, 0x8c, 0xa0, 0x96, 0x4a, 0xa6, 0x91, 0xc8, - 0x97, 0x42, 0x14, 0x88, 0xf2, 0x60, 0x04, 0x76, 0x6a, 0x33, 0xd2, 0x75, 0xe1, 0xfa, 0xf1, 0x0c, 0x53, 0x7b, 0x08, - 0xf4, 0xd8, 0xdb, 0x00, 0x55, 0xa2, 0x2e, 0xc3, 0x72, 0xa2, 0xd0, 0xac, 0x26, 0x59, 0x40, 0x8d, 0x69, 0x83, 0x94, - 0x6c, 0x83, 0x2e, 0x57, 0x80, 0x7e, 0x24, 0x8e, 0x67, 0xb5, 0x03, 0x42, 0xd6, 0xa0, 0x82, 0x21, 0x4f, 0xa9, 0x90, - 0xc2, 0xbc, 0xd7, 0xa5, 0x22, 0x3c, 0x9f, 0x03, 0x2e, 0xb5, 0xe0, 0xcc, 0xcb, 0x68, 0xe0, 0x83, 0xf8, 0x24, 0xc1, - 0xc4, 0x17, 0x5c, 0x15, 0xe8, 0xc1, 0x41, 0xa7, 0xd9, 0x14, 0x28, 0x15, 0x37, 0x29, 0x83, 0x6d, 0x45, 0xae, 0x0d, - 0x3f, 0x24, 0xcb, 0xd6, 0x5d, 0x1e, 0xea, 0x2e, 0x44, 0x02, 0xd8, 0xe9, 0x25, 0x7a, 0xbe, 0x65, 0xbd, 0x74, 0x18, - 0x9c, 0x69, 0x89, 0x83, 0xc0, 0x6f, 0x6f, 0x27, 0xc3, 0x32, 0x25, 0xb2, 0x6b, 0x92, 0xba, 0x80, 0x1c, 0x86, 0x6a, - 0xae, 0x1d, 0x98, 0xa5, 0xd2, 0xc7, 0xf3, 0x72, 0x86, 0xdb, 0xa5, 0x34, 0xe4, 0x66, 0xbc, 0x9a, 0xe6, 0x73, 0x2b, - 0xc9, 0xa6, 0xfd, 0xad, 0xf8, 0xa2, 0xe0, 0x1f, 0x38, 0xb1, 0xd4, 0xea, 0x29, 0xb5, 0xc2, 0xa3, 0xcc, 0x2d, 0x59, - 0xa7, 0xb8, 0x56, 0xd7, 0x0d, 0x54, 0x23, 0x8c, 0xa6, 0x61, 0x23, 0x60, 0x62, 0x82, 0x8a, 0x5f, 0x37, 0x89, 0x98, - 0xce, 0x96, 0xe0, 0x3a, 0x42, 0xef, 0xa1, 0x9c, 0xe0, 0xae, 0xa6, 0xd9, 0xe7, 0xe1, 0xfc, 0x7a, 0xe2, 0xde, 0x37, - 0x88, 0xfb, 0xcb, 0x90, 0x1b, 0x84, 0x1e, 0xcb, 0x84, 0x1f, 0xe9, 0xfb, 0x28, 0x54, 0xd5, 0x93, 0xd3, 0xb0, 0x62, - 0x59, 0xe2, 0xc9, 0x08, 0x75, 0x18, 0x51, 0xd1, 0x1a, 0x23, 0xbb, 0xba, 0xca, 0xcd, 0xb3, 0x40, 0x4e, 0x53, 0x8f, - 0xd7, 0xfd, 0xb4, 0x15, 0x39, 0x1b, 0x9e, 0xc8, 0xfd, 0x57, 0x35, 0x4f, 0x64, 0x45, 0xe7, 0x38, 0xd2, 0x35, 0x81, - 0xdc, 0x27, 0xa7, 0xab, 0x87, 0x54, 0xc8, 0x16, 0xbd, 0x6c, 0xe3, 0x8c, 0xea, 0x80, 0xa4, 0x9e, 0x51, 0x81, 0x55, - 0x8d, 0xbd, 0xb5, 0xd5, 0x11, 0xe9, 0x96, 0x4a, 0xb0, 0xc1, 0xd6, 0xc2, 0x68, 0xc6, 0x28, 0xe8, 0x94, 0x14, 0x19, - 0xa8, 0x51, 0x7e, 0x0d, 0x63, 0xd8, 0xa7, 0x06, 0x20, 0x38, 0xd7, 0x57, 0x7f, 0x59, 0x4a, 0xb2, 0x10, 0x90, 0xb8, - 0x4b, 0x06, 0x6c, 0x4d, 0x10, 0x33, 0xd2, 0xc9, 0x7b, 0xa0, 0xbc, 0x01, 0x43, 0x1b, 0x01, 0xec, 0x02, 0x71, 0xe8, - 0x41, 0xc5, 0xb6, 0x09, 0x29, 0x3a, 0x36, 0xf0, 0x1c, 0x80, 0x9d, 0x57, 0xae, 0xd1, 0x77, 0x55, 0x0a, 0x18, 0x92, - 0x81, 0x1b, 0xb0, 0xca, 0x2d, 0xb7, 0xff, 0x1c, 0xcc, 0x06, 0x78, 0x7d, 0x26, 0x9b, 0x6f, 0x62, 0x9e, 0x60, 0x15, - 0xbb, 0xf0, 0x2b, 0xcd, 0x5a, 0xc4, 0x9d, 0x0e, 0x1b, 0xf5, 0x0a, 0x13, 0xa2, 0xf6, 0x00, 0x6b, 0xdf, 0xa3, 0x87, - 0x45, 0xbc, 0xbf, 0xc2, 0x77, 0x3d, 0x6e, 0xb9, 0xaf, 0x97, 0x45, 0x2b, 0x5d, 0x45, 0x8d, 0x81, 0xc9, 0xba, 0x9d, - 0x8c, 0x6b, 0x2f, 0x0f, 0x84, 0x2f, 0xb8, 0x5a, 0x23, 0xab, 0x5c, 0x8a, 0x8d, 0x45, 0xd2, 0xd3, 0x3e, 0x05, 0xd8, - 0x37, 0x9b, 0xbd, 0x00, 0x33, 0xef, 0x2b, 0x54, 0x49, 0x48, 0x69, 0x76, 0x83, 0x25, 0x09, 0x6d, 0x45, 0x46, 0x9d, - 0x0f, 0x1c, 0x6d, 0x73, 0x2b, 0x8e, 0x60, 0x38, 0x27, 0x61, 0x3a, 0x56, 0x1e, 0x36, 0x19, 0xb8, 0xf2, 0x8e, 0x98, - 0xb6, 0x09, 0xf0, 0x6f, 0x06, 0x7c, 0x7b, 0x25, 0xb9, 0xb6, 0xd0, 0x30, 0x3c, 0x41, 0x84, 0x55, 0x9e, 0x08, 0x34, - 0x14, 0x60, 0x8d, 0x6b, 0x2d, 0x0f, 0x50, 0xe1, 0x6b, 0x67, 0x13, 0x00, 0x12, 0x59, 0x41, 0xce, 0x8a, 0xa3, 0x1b, - 0x56, 0xb9, 0xde, 0x4f, 0x8d, 0x82, 0xc4, 0xc5, 0x83, 0xe9, 0xea, 0x96, 0xfe, 0x0c, 0x35, 0x67, 0x52, 0xc4, 0xb4, - 0x13, 0x04, 0xfd, 0xa3, 0xcc, 0xc9, 0x69, 0x3a, 0xa1, 0x7d, 0xce, 0x9d, 0xda, 0xd4, 0x3d, 0x46, 0xdd, 0x3c, 0x89, - 0x2d, 0x5e, 0xc7, 0x4d, 0x29, 0x17, 0x26, 0x39, 0xe6, 0xa6, 0x48, 0xc5, 0x66, 0x8a, 0xdd, 0xb9, 0xf5, 0x83, 0x16, - 0xd2, 0x41, 0xdb, 0x14, 0x39, 0xd8, 0xac, 0xe2, 0xf7, 0x04, 0xc6, 0x73, 0x81, 0xf8, 0xf2, 0x15, 0x25, 0xe9, 0x30, - 0xc7, 0x5c, 0x60, 0xf5, 0x62, 0x0a, 0xf2, 0x77, 0x8e, 0x4e, 0xb3, 0x37, 0xf0, 0x41, 0xe2, 0x0d, 0x38, 0x66, 0x8d, - 0x7d, 0xe7, 0x52, 0x51, 0x47, 0x08, 0x54, 0x46, 0xb5, 0x4c, 0xc7, 0x89, 0x95, 0xfb, 0x46, 0xd0, 0xd5, 0x5b, 0x1d, - 0xce, 0x37, 0x9e, 0x1b, 0xbb, 0x11, 0xc4, 0x60, 0x2d, 0x14, 0x43, 0x4f, 0xb2, 0xf0, 0x1c, 0xb6, 0x67, 0x7b, 0xbb, - 0x57, 0xec, 0xf1, 0xca, 0x45, 0x52, 0xc1, 0x18, 0x63, 0x46, 0x31, 0x9e, 0x89, 0x9a, 0x58, 0x44, 0x64, 0xcb, 0xd6, - 0x61, 0x81, 0x01, 0x00, 0x68, 0x69, 0x72, 0xaf, 0x9a, 0x08, 0x95, 0xf1, 0x5c, 0x5a, 0x4f, 0x15, 0x44, 0x55, 0x8d, - 0xdf, 0xae, 0xcf, 0x40, 0x21, 0xb8, 0x37, 0x3a, 0x1e, 0x06, 0x21, 0x60, 0x17, 0x05, 0x2f, 0xd0, 0x07, 0xb4, 0x57, - 0x25, 0x42, 0x31, 0x73, 0xb2, 0x1e, 0x33, 0x8c, 0x54, 0xd0, 0x85, 0x4a, 0xd8, 0x2a, 0xcd, 0xf0, 0xab, 0x83, 0xd0, - 0x8c, 0x32, 0xee, 0xbf, 0xaa, 0xd6, 0x0c, 0xf2, 0x83, 0x79, 0xab, 0x84, 0xfa, 0x76, 0x25, 0x22, 0x53, 0x81, 0x89, - 0x87, 0x59, 0x4a, 0xbf, 0x5f, 0xd6, 0x49, 0x3f, 0x2f, 0x97, 0xe7, 0x9c, 0x24, 0x5f, 0xe7, 0x0e, 0x92, 0x4f, 0xba, - 0xfb, 0x95, 0xf0, 0x43, 0x0d, 0xa3, 0x26, 0xfc, 0xea, 0x5b, 0x1a, 0xe6, 0x9e, 0x72, 0x6f, 0xf5, 0xbb, 0xc8, 0x74, - 0x51, 0x9e, 0x83, 0x22, 0xa4, 0x1f, 0xc1, 0x34, 0x34, 0x68, 0x50, 0x24, 0x8b, 0xc5, 0xda, 0x04, 0x71, 0x7d, 0xcc, - 0xa9, 0x76, 0x28, 0x63, 0x8c, 0x68, 0x5a, 0x52, 0x90, 0x24, 0x70, 0x50, 0x7e, 0x03, 0x03, 0x62, 0x12, 0x12, 0xd2, - 0x20, 0x74, 0xd6, 0x66, 0x22, 0x2a, 0x73, 0xf1, 0x76, 0xe5, 0xb2, 0x26, 0x50, 0x84, 0x9e, 0x60, 0xa6, 0x52, 0x2a, - 0x08, 0xa4, 0xca, 0xb7, 0xd1, 0xa9, 0x39, 0x43, 0x73, 0xd7, 0x14, 0x40, 0x5e, 0xdb, 0xf5, 0xa0, 0xc9, 0x7b, 0xf2, - 0xa1, 0xaf, 0x13, 0x23, 0x5e, 0x66, 0xd0, 0x35, 0x1c, 0xfe, 0x1a, 0x2b, 0x29, 0x42, 0x26, 0x7c, 0xaf, 0x60, 0x13, - 0x21, 0x99, 0x82, 0x9e, 0x09, 0xf8, 0x43, 0xbd, 0xb2, 0x97, 0xee, 0xe5, 0x95, 0x49, 0x8b, 0xca, 0x56, 0xa2, 0x66, - 0x2d, 0x8e, 0xe2, 0xed, 0x14, 0xce, 0xb3, 0x47, 0x09, 0x04, 0x24, 0xa9, 0x9c, 0xa4, 0x9a, 0xf7, 0x28, 0x1d, 0x02, - 0x48, 0x70, 0xfa, 0x09, 0x2c, 0xb4, 0x9b, 0x12, 0x13, 0x2c, 0xaa, 0xc6, 0x6e, 0x73, 0x90, 0x9a, 0x73, 0x92, 0x7c, - 0x73, 0x94, 0xda, 0xdb, 0x4a, 0x7b, 0xc6, 0xec, 0x00, 0xdb, 0x76, 0xb7, 0xf3, 0xa3, 0x74, 0xbb, 0x33, 0x34, 0x18, - 0x17, 0x86, 0xff, 0x93, 0x12, 0xd3, 0x40, 0x0a, 0x29, 0x1b, 0x3f, 0xa1, 0x0c, 0xc3, 0xff, 0x96, 0x24, 0x80, 0x07, - 0xb5, 0xdd, 0x58, 0x31, 0xee, 0x15, 0x45, 0xc9, 0x6d, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0xf4, 0x89, 0x62, - 0x9e, 0x13, 0x00, 0xa3, 0xc8, 0xfc, 0x1d, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4d, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0x4f, - 0x29, 0xa4, 0xa2, 0xb2, 0x39, 0x89, 0xf8, 0x77, 0x05, 0x98, 0xe6, 0xc4, 0x47, 0x7a, 0xae, 0x61, 0x28, 0xc0, 0x57, - 0x3a, 0x94, 0x9a, 0xed, 0xe9, 0x1f, 0x9d, 0xed, 0xbe, 0x44, 0x8a, 0x20, 0x81, 0x06, 0x5e, 0xae, 0x59, 0x2f, 0xac, - 0x32, 0xb8, 0x23, 0xfe, 0x14, 0x7c, 0x5f, 0x5e, 0x07, 0x9f, 0x71, 0xfe, 0x05, 0xa0, 0x55, 0x81, 0x01, 0xe5, 0x83, - 0xa6, 0x62, 0x25, 0xd8, 0x25, 0x0a, 0xcc, 0xca, 0xcf, 0x1f, 0xd7, 0x69, 0xdd, 0xd4, 0x2c, 0xd1, 0x29, 0x3f, 0x77, - 0x0d, 0x33, 0xbe, 0xd7, 0xc8, 0x1f, 0xdf, 0x7f, 0x0e, 0xb2, 0x9d, 0x50, 0xbb, 0xb5, 0x55, 0x6c, 0x90, 0x86, 0x86, - 0xf7, 0xc2, 0xe6, 0xd0, 0x16, 0xf1, 0x52, 0xa8, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x87, - 0x15, 0x82, 0xc5, 0x4e, 0x65, 0xf2, 0x19, 0x0e, 0x9a, 0x22, 0xd7, 0x42, 0x28, 0x7c, 0x39, 0x88, 0x4a, 0x49, 0x8b, - 0x75, 0xb4, 0x3d, 0x3b, 0x83, 0xe7, 0x97, 0x71, 0x01, 0xd8, 0x81, 0xe5, 0x57, 0x58, 0x16, 0x07, 0xc8, 0xc5, 0x43, - 0x59, 0xeb, 0x15, 0x8d, 0xc7, 0x37, 0x76, 0x61, 0x75, 0x01, 0x3e, 0x8d, 0xd2, 0x71, 0x22, 0x26, 0x31, 0x93, 0x2a, - 0xd7, 0xe4, 0xda, 0xe8, 0x5e, 0x5a, 0xa3, 0x79, 0x2e, 0x38, 0x78, 0x85, 0xe0, 0x06, 0xd3, 0x57, 0xf2, 0x72, 0xbd, - 0x82, 0x82, 0xa1, 0xf6, 0xe6, 0x26, 0x98, 0x2b, 0xf1, 0x98, 0xc1, 0x35, 0xfd, 0x3a, 0x9c, 0x8a, 0x6e, 0x5e, 0xae, - 0x18, 0xfc, 0x3a, 0x67, 0xac, 0x21, 0x00, 0x88, 0x4e, 0x1e, 0x5e, 0x6f, 0x26, 0xbd, 0x52, 0xd2, 0x41, 0x49, 0x84, - 0xf8, 0xae, 0xcc, 0xd7, 0x5d, 0x2a, 0xba, 0x72, 0xd5, 0xbd, 0xaf, 0x19, 0x33, 0x2e, 0x18, 0x3d, 0xe7, 0xb3, 0xa4, - 0x71, 0xed, 0x86, 0xee, 0xea, 0xfc, 0xe8, 0xfd, 0x20, 0xf3, 0x16, 0x66, 0x40, 0x26, 0x20, 0x0a, 0x9e, 0x7b, 0xaf, - 0x8d, 0x88, 0xf2, 0xb7, 0x66, 0x88, 0x57, 0x0e, 0xb3, 0x2e, 0x92, 0xfc, 0xed, 0xe0, 0xdb, 0xe0, 0xfa, 0x96, 0x46, - 0x04, 0xb9, 0xab, 0x22, 0xc8, 0x84, 0xb9, 0x99, 0x3e, 0x70, 0xfb, 0x77, 0x65, 0x08, 0x22, 0x2a, 0xa6, 0x43, 0xe5, - 0xb8, 0x7f, 0xb4, 0x41, 0xa5, 0x42, 0xe2, 0x53, 0x95, 0xbb, 0x72, 0x6d, 0x6a, 0xa8, 0xc7, 0x75, 0x32, 0x0b, 0x4d, - 0xb3, 0x26, 0x97, 0xb2, 0x69, 0x31, 0x32, 0x4d, 0x4e, 0xb5, 0xf9, 0xdd, 0x6b, 0x83, 0x74, 0x0c, 0xd5, 0xc5, 0x5a, - 0x2d, 0x98, 0xdf, 0x95, 0x17, 0xde, 0xf5, 0x62, 0x23, 0x95, 0xa1, 0xa6, 0x3d, 0x8a, 0x3e, 0x8e, 0xdb, 0xcc, 0xe5, - 0x51, 0xfa, 0x67, 0x0d, 0x00, 0xd3, 0x10, 0x16, 0xdd, 0x4d, 0xcb, 0xd8, 0x13, 0xcb, 0xd3, 0x13, 0x19, 0x28, 0x7a, - 0xae, 0xf3, 0x55, 0xab, 0xc4, 0xd2, 0x35, 0x08, 0x76, 0x6f, 0xc8, 0x58, 0x95, 0xb8, 0x5b, 0xad, 0x5f, 0xcd, 0xf3, - 0x79, 0xca, 0x57, 0xf2, 0x7c, 0x6a, 0x1a, 0xdd, 0x46, 0xdb, 0xbd, 0x39, 0x35, 0x54, 0xcc, 0xb5, 0xbe, 0xc9, 0x1f, - 0x98, 0xae, 0x83, 0xae, 0x16, 0x81, 0x66, 0x75, 0xaa, 0x9e, 0x95, 0xe5, 0xac, 0x9e, 0xc9, 0x31, 0x13, 0xb6, 0xa9, - 0x34, 0x87, 0xe8, 0x86, 0xa9, 0x9a, 0xe9, 0xc7, 0xc6, 0xb1, 0x90, 0x6d, 0x9e, 0x5f, 0x8e, 0x73, 0xc0, 0xb4, 0x3c, - 0x5f, 0x26, 0x0c, 0x3f, 0x5e, 0x5d, 0xfd, 0x28, 0xf8, 0x54, 0xd5, 0xd1, 0x5b, 0xbe, 0xd4, 0x3d, 0x83, 0x59, 0xa9, - 0x8c, 0x88, 0x13, 0xb6, 0x7e, 0xf0, 0xe6, 0xe9, 0x15, 0xb0, 0x9c, 0xc0, 0xea, 0x4e, 0x98, 0xd3, 0x18, 0xaa, 0x3a, - 0xc0, 0x3f, 0xac, 0x1f, 0x6c, 0xdd, 0x19, 0xfe, 0x61, 0xf0, 0x43, 0x70, 0x63, 0x63, 0xe3, 0x18, 0xef, 0xd6, 0x12, - 0x41, 0x5e, 0x61, 0x40, 0x1f, 0xaf, 0x3e, 0x0a, 0x5c, 0xae, 0x63, 0xdb, 0x03, 0x87, 0xdc, 0xd6, 0xc0, 0xdf, 0x24, - 0x4f, 0x1a, 0x2d, 0x0a, 0x9e, 0xcd, 0xe4, 0x0c, 0x85, 0xbc, 0xe6, 0xe3, 0xa0, 0xee, 0x08, 0x7f, 0x03, 0xa7, 0x16, - 0x5e, 0x5e, 0x7e, 0x82, 0x3e, 0x60, 0xe9, 0x4a, 0x6e, 0x2a, 0xfc, 0x94, 0xf2, 0x88, 0xae, 0xd6, 0x79, 0x30, 0x52, - 0x5c, 0x4c, 0x51, 0xe8, 0xb8, 0xcb, 0x1b, 0x67, 0x23, 0xa3, 0xbf, 0xc4, 0xab, 0x8b, 0x74, 0xf9, 0x48, 0x64, 0xab, - 0x96, 0xde, 0x2f, 0x3a, 0xba, 0x6d, 0xcf, 0x18, 0x9f, 0x66, 0x63, 0x0a, 0xcc, 0xf8, 0x38, 0x11, 0x5e, 0x9f, 0x18, - 0xeb, 0xbb, 0x45, 0xa0, 0xba, 0x39, 0x36, 0xd9, 0xe1, 0x78, 0xbd, 0xd9, 0xac, 0x71, 0x07, 0x6f, 0x9c, 0x27, 0xce, - 0xb2, 0x44, 0x8f, 0xca, 0x52, 0xc3, 0x03, 0x52, 0x21, 0x6e, 0xde, 0x33, 0x81, 0x71, 0xd9, 0x25, 0x71, 0x6d, 0x37, - 0x10, 0x6b, 0xb1, 0x27, 0x31, 0x4b, 0xc6, 0xb6, 0x07, 0xe5, 0x81, 0xbe, 0x18, 0x4d, 0xb7, 0x80, 0x69, 0x7b, 0xed, - 0xec, 0x3c, 0xb5, 0xbd, 0x6a, 0xaa, 0x00, 0x66, 0xc9, 0xf2, 0xf8, 0x14, 0x49, 0xf7, 0x1b, 0xe8, 0x22, 0x06, 0x8c, - 0x8d, 0x2b, 0x73, 0xee, 0x72, 0xdd, 0x8e, 0xf8, 0x46, 0x13, 0xa9, 0x52, 0x1f, 0x51, 0xdf, 0x61, 0x58, 0xab, 0xab, - 0x0c, 0x24, 0x30, 0x8f, 0xbc, 0x3b, 0xae, 0xa5, 0xa7, 0x63, 0x16, 0x93, 0x2a, 0x7d, 0x4b, 0x5d, 0x8b, 0x6b, 0xba, - 0xbd, 0xe2, 0x01, 0xe8, 0x1f, 0xe8, 0xb7, 0x88, 0x85, 0xbf, 0x9d, 0xd7, 0x52, 0x58, 0x1b, 0x73, 0xe4, 0xe8, 0x6b, - 0x0f, 0x7e, 0x61, 0xd5, 0x9e, 0x81, 0x1a, 0x66, 0xc4, 0x48, 0x7e, 0x33, 0xee, 0x55, 0x4d, 0x1c, 0xb9, 0x0b, 0xc0, - 0xfa, 0x96, 0x74, 0x49, 0x0e, 0xaf, 0x64, 0xb9, 0x2a, 0x86, 0xfc, 0x1b, 0xec, 0xb3, 0xde, 0x9c, 0x80, 0x99, 0x38, - 0xe5, 0x25, 0x26, 0xa6, 0x88, 0xcb, 0xcd, 0xd2, 0xe7, 0x69, 0xda, 0x2c, 0xda, 0xc0, 0x29, 0x8c, 0x04, 0x8e, 0xd8, - 0x37, 0xb6, 0xa1, 0x99, 0xb0, 0x11, 0x13, 0x6a, 0x54, 0x4a, 0x09, 0x1f, 0xc8, 0xad, 0x96, 0xf4, 0x65, 0x6e, 0xaf, - 0xbe, 0xdc, 0x26, 0x28, 0xa0, 0xa8, 0x81, 0xe5, 0xd0, 0x38, 0x6e, 0x19, 0xc8, 0x85, 0xc5, 0xb0, 0x30, 0x6a, 0x55, - 0xae, 0x26, 0xa3, 0x3a, 0x99, 0xaf, 0x16, 0x17, 0x2a, 0xf4, 0xe0, 0x91, 0x40, 0xce, 0x5f, 0x60, 0xea, 0x60, 0x56, - 0x6a, 0x33, 0x2d, 0x36, 0x51, 0xde, 0x33, 0x1d, 0x92, 0xeb, 0xaf, 0xe1, 0xa1, 0xf2, 0x8b, 0x57, 0xe6, 0x14, 0xf3, - 0x45, 0x1e, 0x4b, 0x5b, 0x63, 0x6e, 0xfd, 0xaf, 0xf2, 0x3e, 0xad, 0x04, 0xec, 0x37, 0x60, 0x53, 0xc6, 0x5a, 0x62, - 0xe3, 0x82, 0xa4, 0xbc, 0x96, 0xa7, 0xf4, 0xbe, 0x86, 0xf0, 0x5d, 0x51, 0xe9, 0x2a, 0x91, 0x75, 0x8d, 0x56, 0xf7, - 0xeb, 0x82, 0xe5, 0x97, 0x07, 0x0c, 0x73, 0x93, 0x51, 0x21, 0x5b, 0x51, 0xb3, 0x29, 0xbf, 0xda, 0xbb, 0xf1, 0x2b, - 0x0f, 0x25, 0x05, 0xd5, 0x2a, 0xd9, 0xbc, 0x72, 0xc3, 0x31, 0x6e, 0xdc, 0x70, 0x8c, 0x7b, 0x14, 0x57, 0xae, 0x50, - 0xad, 0xf3, 0xdf, 0x57, 0xdd, 0x4f, 0x74, 0xd6, 0x86, 0xfa, 0xd4, 0x0d, 0xd7, 0xa6, 0xa7, 0xdf, 0xb0, 0x54, 0x23, - 0x4b, 0xe8, 0xa6, 0xa5, 0x62, 0x32, 0x12, 0x25, 0xa6, 0xab, 0x94, 0x47, 0x7d, 0x8d, 0xb8, 0x00, 0x76, 0x43, 0xf9, - 0x8b, 0x7f, 0x0d, 0xcf, 0x8f, 0x03, 0x54, 0xa2, 0x96, 0x93, 0x2c, 0xe5, 0xad, 0x49, 0x34, 0x8b, 0x93, 0xcb, 0x60, - 0x11, 0xb7, 0x66, 0x59, 0x9a, 0x15, 0x73, 0xa0, 0x4a, 0xaf, 0xb8, 0x04, 0x1d, 0x7e, 0xd6, 0x5a, 0xc4, 0xde, 0x73, - 0x96, 0x9c, 0x31, 0x1e, 0x8f, 0x22, 0xcf, 0xde, 0xcf, 0x81, 0x3d, 0x58, 0xaf, 0xa3, 0x3c, 0xcf, 0xce, 0x6d, 0xef, - 0x5d, 0x76, 0x02, 0x44, 0xeb, 0xbd, 0xb9, 0xb8, 0x3c, 0x65, 0xa9, 0xf7, 0xfe, 0x64, 0x91, 0xf2, 0x85, 0x57, 0x44, - 0x69, 0xd1, 0x2a, 0x58, 0x1e, 0x4f, 0x40, 0x4c, 0x24, 0x59, 0xde, 0xc2, 0xfc, 0xe7, 0x19, 0x0b, 0x92, 0xf8, 0x74, - 0xca, 0xad, 0x71, 0x94, 0x7f, 0xea, 0xb5, 0x5a, 0xf3, 0x3c, 0x9e, 0x45, 0xf9, 0x65, 0x8b, 0x5a, 0x04, 0x9f, 0xb5, - 0x77, 0xa3, 0x2f, 0x26, 0xf7, 0x7b, 0x3c, 0x87, 0xbe, 0x31, 0x62, 0x31, 0x00, 0xe6, 0x63, 0xed, 0x3e, 0x68, 0xcf, - 0x8a, 0x0d, 0x11, 0x51, 0x8a, 0x52, 0x5e, 0x1e, 0x7b, 0x1f, 0x41, 0xb7, 0x3d, 0xf6, 0x4f, 0x78, 0xea, 0x81, 0x2d, - 0xc7, 0xb3, 0x74, 0x39, 0x5a, 0xe4, 0x05, 0x0c, 0x30, 0xcf, 0xe2, 0x94, 0xb3, 0xbc, 0x77, 0x92, 0xe5, 0x80, 0xb6, - 0x56, 0x1e, 0x8d, 0xe3, 0x45, 0x11, 0xdc, 0x9f, 0x5f, 0xf4, 0x50, 0x57, 0x38, 0xcd, 0xb3, 0x45, 0x3a, 0x96, 0x73, - 0xc5, 0x29, 0x1c, 0x8c, 0x98, 0x9b, 0x15, 0xf4, 0x25, 0x14, 0x80, 0x2f, 0x65, 0x51, 0xde, 0x3a, 0xc5, 0xce, 0xa8, - 0xe8, 0xb7, 0xc7, 0xec, 0xd4, 0xcb, 0x4f, 0x4f, 0x22, 0xa7, 0xd3, 0x7d, 0xe4, 0xa9, 0x7f, 0xfe, 0x03, 0x17, 0x14, - 0xf7, 0xb5, 0xc5, 0x9d, 0x76, 0xfb, 0x1f, 0xdc, 0x5e, 0x63, 0x16, 0x02, 0x28, 0xe8, 0xcc, 0x2f, 0xac, 0x22, 0x4b, - 0x60, 0x7f, 0xd6, 0xf5, 0xec, 0xcd, 0xc1, 0x6e, 0x8a, 0xd3, 0xd3, 0xa0, 0x3b, 0xbf, 0x28, 0x71, 0x75, 0x81, 0x48, - 0xc8, 0x94, 0x8b, 0x94, 0x6f, 0xcb, 0x3f, 0x0a, 0xf1, 0xe3, 0xf5, 0x10, 0x77, 0x15, 0xc4, 0x15, 0xd6, 0x5b, 0x63, - 0x38, 0x07, 0x84, 0xfe, 0x4e, 0x21, 0x00, 0x99, 0x82, 0x11, 0x98, 0x2b, 0x38, 0xe8, 0xe5, 0x0f, 0x83, 0xd1, 0x5d, - 0x0f, 0xc6, 0xe3, 0xdb, 0xc0, 0xc8, 0xd3, 0xf1, 0xb2, 0xbe, 0xaf, 0x1d, 0x30, 0x4e, 0x7b, 0x53, 0x86, 0xf4, 0x14, - 0x74, 0xf1, 0xf9, 0x3c, 0x1e, 0xf3, 0xa9, 0x78, 0x24, 0x72, 0x3e, 0x17, 0x75, 0x0f, 0xda, 0x6d, 0xf1, 0x5e, 0x80, - 0x40, 0x0b, 0x3a, 0x3e, 0x36, 0x00, 0x22, 0x7a, 0x71, 0xdd, 0x47, 0x6c, 0x3e, 0xdc, 0xfa, 0xa5, 0x1a, 0xef, 0x52, - 0xe5, 0x0d, 0x0a, 0x11, 0xa1, 0xbe, 0xd9, 0x82, 0x19, 0x6f, 0x45, 0xbf, 0xa3, 0x03, 0x55, 0x83, 0x0f, 0x8c, 0xa4, - 0x5e, 0xc0, 0x3d, 0x33, 0x17, 0xa8, 0x97, 0xf6, 0xd1, 0x25, 0xd5, 0x6a, 0xb9, 0x20, 0x37, 0x18, 0xba, 0x90, 0x28, - 0x20, 0xe8, 0x14, 0x83, 0x9c, 0xbe, 0xa9, 0x91, 0xb9, 0x41, 0xee, 0x64, 0x2e, 0x1c, 0xf9, 0x4c, 0xf3, 0xf5, 0x62, - 0x6b, 0x0b, 0xac, 0xec, 0x17, 0x4c, 0x36, 0x00, 0xee, 0x4d, 0xae, 0xae, 0xef, 0x43, 0x61, 0x4a, 0x29, 0x43, 0x6a, - 0x76, 0xd3, 0x15, 0x7d, 0xd8, 0x95, 0x98, 0x32, 0x92, 0x8f, 0x86, 0xff, 0x0e, 0xc5, 0xde, 0xd1, 0x86, 0x65, 0x91, - 0x2d, 0xf2, 0x11, 0x79, 0xea, 0x56, 0x2d, 0x7e, 0x9b, 0x04, 0xae, 0xed, 0x31, 0xcd, 0xe7, 0xd1, 0x0c, 0xae, 0x7d, - 0xe4, 0x80, 0x53, 0x10, 0x44, 0xdc, 0x31, 0x90, 0x5e, 0x0e, 0x05, 0x21, 0x8a, 0xae, 0x31, 0xe5, 0xbb, 0xd1, 0xfd, - 0x4b, 0x7f, 0x91, 0xc6, 0xc0, 0xe9, 0x3e, 0xc6, 0x63, 0xba, 0x77, 0x12, 0x8f, 0x29, 0x10, 0xd1, 0xa2, 0xc4, 0x23, - 0xf4, 0x6c, 0x43, 0x81, 0xfa, 0x0e, 0x0b, 0x3c, 0xcb, 0x44, 0x16, 0xbb, 0x65, 0x63, 0x30, 0xc1, 0x10, 0x95, 0xe3, - 0x6c, 0x16, 0xc5, 0x69, 0x80, 0xdf, 0x07, 0xf1, 0xf4, 0x88, 0x01, 0x76, 0xf1, 0xe0, 0x27, 0x93, 0xb9, 0x68, 0x1d, - 0xd7, 0xff, 0x05, 0xf8, 0x08, 0xf5, 0x2f, 0xa5, 0x1d, 0xa6, 0xe1, 0x52, 0x61, 0xde, 0x7a, 0x29, 0xf0, 0x1e, 0xae, - 0x74, 0x56, 0x46, 0x7e, 0x8e, 0x3d, 0x4e, 0x3f, 0x06, 0xad, 0x4e, 0xd0, 0xd1, 0xa6, 0x6b, 0xed, 0x36, 0xaa, 0xc8, - 0x65, 0x91, 0x37, 0x1a, 0x09, 0x06, 0xfd, 0x2c, 0xe0, 0xac, 0xde, 0x35, 0xac, 0x9e, 0xa4, 0x4b, 0x74, 0xe0, 0x9c, - 0xa6, 0x4e, 0x0d, 0x08, 0x8a, 0x05, 0x5c, 0x33, 0x95, 0x5b, 0x46, 0x24, 0x94, 0xbe, 0xa4, 0x03, 0x5c, 0xbf, 0x4b, - 0x84, 0xf7, 0x86, 0xea, 0x29, 0x50, 0x8a, 0xe4, 0x16, 0xc7, 0x7b, 0xe2, 0xc4, 0x5b, 0x44, 0x63, 0xa1, 0x0d, 0x47, - 0xd0, 0xb6, 0xfe, 0x32, 0x02, 0x2c, 0x7d, 0x0a, 0xed, 0xcd, 0xa5, 0xa3, 0x12, 0xeb, 0x73, 0x98, 0x6b, 0x5f, 0x48, - 0x3d, 0xba, 0x91, 0x6f, 0xf7, 0x37, 0x97, 0xbc, 0xdc, 0xdb, 0x11, 0xbd, 0xfb, 0xc7, 0x65, 0x41, 0x02, 0xca, 0x74, - 0xa4, 0x55, 0x53, 0x88, 0x3a, 0x18, 0x96, 0xd2, 0x77, 0x71, 0xdc, 0x42, 0x2b, 0x5d, 0xc2, 0x63, 0x2c, 0xc9, 0x2e, - 0xc7, 0x74, 0xa5, 0x28, 0x87, 0x33, 0xa9, 0x13, 0x52, 0x72, 0x91, 0x83, 0xd1, 0x5b, 0x85, 0xe2, 0x18, 0x21, 0x18, - 0x6c, 0x2e, 0xe3, 0x32, 0xdc, 0x5c, 0x66, 0xe5, 0x31, 0x68, 0x26, 0x08, 0x55, 0xa1, 0x3e, 0xef, 0x02, 0x13, 0x0b, - 0x27, 0x8b, 0x45, 0x23, 0xe0, 0xb4, 0xac, 0xb4, 0xad, 0x81, 0x80, 0x06, 0x2c, 0x40, 0x2c, 0x00, 0xdd, 0x8d, 0x7a, - 0x31, 0x58, 0x8b, 0x68, 0xdd, 0x87, 0x81, 0xf6, 0x76, 0x44, 0x23, 0x58, 0x57, 0x8e, 0x20, 0x57, 0xcb, 0xc2, 0x74, - 0x1c, 0x73, 0x69, 0x49, 0x74, 0xc2, 0x12, 0xe8, 0x9f, 0x5f, 0x5d, 0xb5, 0xa1, 0x9b, 0x78, 0xb5, 0xf6, 0xe2, 0x74, - 0xbe, 0x90, 0xdf, 0xd4, 0x82, 0x59, 0x3a, 0x18, 0xe6, 0xc4, 0x94, 0xff, 0x81, 0x8a, 0xdb, 0x05, 0x36, 0x8d, 0x6b, - 0x03, 0x3c, 0x14, 0x32, 0x40, 0x50, 0x2a, 0x1a, 0x80, 0xd2, 0x78, 0xbc, 0x5a, 0xa6, 0x97, 0x51, 0xc0, 0x0b, 0x9c, - 0xc1, 0x39, 0x3e, 0xa7, 0xf0, 0x3c, 0x8b, 0x53, 0x7c, 0xcc, 0xf1, 0x31, 0xba, 0xc0, 0xc7, 0xac, 0xb4, 0xff, 0x2e, - 0xe8, 0xb6, 0x34, 0x02, 0xb2, 0xab, 0x2b, 0x60, 0xee, 0x1a, 0x05, 0x40, 0x10, 0xe2, 0xdb, 0x2a, 0xcc, 0xc4, 0x16, - 0x2b, 0xe6, 0x2d, 0x51, 0x6e, 0x91, 0xf0, 0x0c, 0xc1, 0xb6, 0xca, 0x9d, 0x86, 0x8e, 0xe0, 0xc9, 0x2c, 0x92, 0x27, - 0xf8, 0xe2, 0xda, 0x96, 0xf8, 0xf8, 0x85, 0x40, 0x07, 0x3d, 0xe2, 0xda, 0x74, 0x19, 0x97, 0x9f, 0xb5, 0x89, 0x43, - 0x1b, 0x67, 0x01, 0x35, 0x0d, 0x99, 0x3d, 0x8f, 0xe2, 0x44, 0x34, 0x5e, 0xb3, 0x92, 0x46, 0x3a, 0x20, 0x2d, 0x64, - 0x6f, 0xa7, 0x82, 0x0d, 0x80, 0x1f, 0x89, 0xcb, 0xd4, 0x15, 0xf4, 0xb6, 0xa8, 0xa2, 0x28, 0xb9, 0x3c, 0xbc, 0x03, - 0xe1, 0x0f, 0xd7, 0xeb, 0x1c, 0x82, 0x5d, 0x17, 0xa5, 0xf5, 0x16, 0x00, 0xf1, 0x9c, 0xb1, 0xb1, 0x67, 0x5b, 0xc0, - 0x26, 0xc5, 0xf3, 0xc7, 0x84, 0x9d, 0x31, 0xf9, 0x11, 0x14, 0xdd, 0x57, 0x57, 0x8e, 0x40, 0xda, 0x72, 0x79, 0x3f, - 0x53, 0x52, 0x9e, 0x5a, 0x97, 0x5c, 0x7d, 0x1d, 0x78, 0xcf, 0x36, 0x06, 0x6d, 0xce, 0xd1, 0xae, 0x0f, 0xeb, 0x75, - 0x40, 0x91, 0xb5, 0x01, 0x4c, 0xd2, 0xcf, 0x6e, 0x5a, 0x0a, 0xf4, 0x63, 0x93, 0x09, 0x1c, 0x00, 0x15, 0xb7, 0xd0, - 0xa7, 0x5b, 0x00, 0x03, 0x66, 0xa6, 0x67, 0x8b, 0x16, 0x76, 0xd5, 0x56, 0x3f, 0x21, 0x2a, 0x92, 0x6c, 0xf4, 0xa9, - 0x36, 0xc5, 0x02, 0x09, 0x08, 0xc7, 0x6a, 0xf0, 0x29, 0xfb, 0xdf, 0xfe, 0xf5, 0x7f, 0xfe, 0x57, 0x18, 0x8e, 0x3a, - 0xb8, 0xa5, 0x75, 0x7d, 0xab, 0xff, 0x01, 0xad, 0x16, 0xe9, 0x2d, 0xed, 0xfe, 0xf6, 0xcf, 0xff, 0x0d, 0x9a, 0xd1, - 0xcd, 0x1a, 0xb7, 0x3c, 0x0e, 0xec, 0x11, 0x6a, 0x32, 0x77, 0x03, 0xa4, 0xd6, 0xf5, 0xda, 0xf1, 0xff, 0x05, 0x81, - 0x2d, 0x78, 0x36, 0xbf, 0x11, 0x08, 0x84, 0x75, 0x94, 0x64, 0x05, 0x13, 0x50, 0x08, 0x36, 0x79, 0x47, 0x30, 0x68, - 0x86, 0x39, 0x90, 0x6c, 0x61, 0x89, 0xde, 0x02, 0xfb, 0xb5, 0xde, 0x8d, 0x5d, 0x29, 0x18, 0x27, 0xd0, 0xc9, 0x03, - 0x00, 0xfb, 0x20, 0x9e, 0xe0, 0x81, 0x4e, 0x33, 0x6c, 0xbb, 0xce, 0x17, 0x68, 0x0c, 0xa1, 0x89, 0x4c, 0x8c, 0x20, - 0x5c, 0x1d, 0xaa, 0x1f, 0xfc, 0x04, 0xd6, 0xf2, 0x51, 0x3f, 0x47, 0x17, 0xfa, 0x19, 0xd9, 0x0f, 0x0c, 0x0b, 0x82, - 0x62, 0x86, 0x3a, 0x40, 0x73, 0x61, 0xea, 0xa4, 0x56, 0xfc, 0x81, 0xa9, 0xe4, 0xb0, 0x8f, 0x98, 0x0f, 0x89, 0xb7, - 0x5f, 0x16, 0x39, 0xab, 0x38, 0x26, 0x36, 0x10, 0xac, 0xc8, 0xac, 0xff, 0x98, 0x64, 0xe7, 0xe5, 0x75, 0x75, 0x53, - 0xa0, 0xe2, 0x72, 0x6f, 0x1c, 0x9f, 0xf5, 0x25, 0x22, 0x1b, 0x6b, 0x59, 0xed, 0xd2, 0x5c, 0x18, 0x56, 0xc9, 0x75, - 0xc9, 0x47, 0x5c, 0x96, 0xd7, 0x46, 0x01, 0x80, 0xe3, 0xee, 0x9d, 0xe4, 0x7d, 0xb9, 0x80, 0x57, 0x78, 0x61, 0x8b, - 0x20, 0x41, 0x3e, 0x2e, 0x64, 0x0c, 0x27, 0x19, 0x63, 0xb2, 0x7a, 0xd4, 0x5a, 0x33, 0xc5, 0xd2, 0xb1, 0x61, 0x8d, - 0x0b, 0x73, 0xc9, 0x85, 0x63, 0xa9, 0x0e, 0x49, 0x2e, 0x8c, 0x1f, 0xe0, 0x68, 0x70, 0xe1, 0xf8, 0x5a, 0x2e, 0x8c, - 0x6b, 0x1b, 0xe0, 0xc8, 0xa1, 0xbd, 0x8d, 0xb6, 0xb8, 0x21, 0x15, 0x38, 0x0a, 0x37, 0x30, 0xc0, 0x46, 0x9f, 0xa4, - 0x6c, 0x23, 0x50, 0xd1, 0x18, 0x95, 0xd2, 0x9a, 0x04, 0x9b, 0x64, 0xcf, 0xc1, 0xe2, 0x18, 0x64, 0x9b, 0x39, 0x32, - 0x58, 0xc2, 0x13, 0x86, 0xc7, 0xff, 0x78, 0x07, 0xfb, 0x8a, 0xcd, 0x2c, 0xe9, 0x19, 0xa4, 0xcf, 0x0e, 0x0d, 0xe0, - 0x2d, 0x85, 0x3b, 0x23, 0xb0, 0xdf, 0xbe, 0x39, 0x38, 0xb4, 0xbd, 0x93, 0x6c, 0x7c, 0x19, 0xd8, 0xa0, 0x8a, 0x82, - 0x24, 0x73, 0x7d, 0x3e, 0x65, 0xa9, 0xa3, 0x94, 0xc1, 0x2c, 0x01, 0x65, 0x38, 0x3b, 0x15, 0xb7, 0xef, 0x9b, 0xae, - 0x58, 0x40, 0x1b, 0x7d, 0x9e, 0xaf, 0xbf, 0xc7, 0xc5, 0x97, 0x2b, 0x79, 0x8e, 0x8f, 0x7d, 0x0c, 0x46, 0xef, 0xed, - 0xc0, 0x03, 0xbe, 0x1c, 0x20, 0x05, 0xe9, 0x37, 0x01, 0x67, 0x21, 0xde, 0x77, 0xb0, 0xfd, 0x8e, 0xea, 0x8b, 0x50, - 0x28, 0x1a, 0xd0, 0xfa, 0x5a, 0xa5, 0x04, 0xd0, 0xd8, 0x63, 0x22, 0x41, 0xdc, 0x18, 0xc0, 0x01, 0x1f, 0xeb, 0x12, - 0x41, 0xa6, 0x46, 0x11, 0x8d, 0x52, 0xb1, 0x7f, 0x59, 0x85, 0x13, 0x12, 0xfa, 0xc4, 0x64, 0xf0, 0x93, 0xc0, 0x3f, - 0x36, 0xbf, 0x34, 0x25, 0x3e, 0x0a, 0xa3, 0x17, 0x79, 0xf4, 0x57, 0xb0, 0x61, 0xbd, 0xf3, 0x63, 0x6a, 0xa9, 0xcc, - 0x1a, 0xb4, 0xb7, 0xd1, 0xfc, 0x6b, 0x2b, 0xfb, 0x15, 0x24, 0x5e, 0x12, 0xcd, 0x0b, 0x16, 0xa8, 0x07, 0x69, 0xe1, - 0xa0, 0xa1, 0xb4, 0x6a, 0x52, 0x9a, 0x92, 0xb1, 0xe4, 0xd3, 0xa5, 0x69, 0x02, 0x3d, 0x04, 0x13, 0x08, 0xd3, 0xb7, - 0x5b, 0x11, 0xb0, 0xf7, 0x34, 0x48, 0xd8, 0x84, 0x97, 0x1c, 0xef, 0x07, 0x2f, 0x95, 0xcd, 0xe9, 0x77, 0x1f, 0x80, - 0x59, 0x64, 0xf9, 0xf8, 0xff, 0x6d, 0x63, 0x8f, 0x83, 0x14, 0xcc, 0x18, 0xba, 0x30, 0x80, 0x97, 0xb1, 0x00, 0x22, - 0xf3, 0x7d, 0x69, 0x4c, 0x34, 0x62, 0x68, 0x8f, 0x97, 0x3c, 0xb7, 0xf8, 0xd4, 0xe3, 0xb9, 0xd9, 0x0e, 0x34, 0xa5, - 0x15, 0xa3, 0x7c, 0xd5, 0x2c, 0xdc, 0x75, 0xa5, 0xf2, 0xb8, 0xda, 0x58, 0xd9, 0xd6, 0xf5, 0xb7, 0x15, 0x0c, 0x19, - 0x5e, 0x80, 0x52, 0x70, 0xbe, 0xa5, 0xe8, 0x61, 0xae, 0x69, 0xd5, 0x3f, 0x70, 0xab, 0xee, 0x51, 0xd2, 0xd9, 0x3e, - 0xa2, 0xb3, 0x4d, 0xcc, 0x65, 0xb8, 0x14, 0x73, 0x8f, 0xa2, 0x64, 0xe4, 0x20, 0x00, 0x56, 0xcb, 0xba, 0x0f, 0xd8, - 0x04, 0x2e, 0x3d, 0x2c, 0xcb, 0xde, 0x25, 0x73, 0x8e, 0x7e, 0x93, 0x79, 0xe4, 0xe2, 0xfa, 0xa0, 0xfe, 0x04, 0x5b, - 0xbb, 0x74, 0x87, 0xde, 0xf7, 0xc6, 0x77, 0xad, 0x6c, 0x45, 0xa9, 0xb6, 0x07, 0xf8, 0xfd, 0x3e, 0xc4, 0xbe, 0xaf, - 0x1c, 0x1b, 0xb5, 0x10, 0xaa, 0xb9, 0x6c, 0x11, 0xe1, 0xd8, 0xd8, 0x4d, 0x78, 0x41, 0xbf, 0xba, 0xce, 0x98, 0xfd, - 0xee, 0x76, 0x63, 0x96, 0xdd, 0xd1, 0x98, 0xfd, 0xee, 0x4f, 0x36, 0x66, 0xbf, 0x6a, 0x1a, 0xb3, 0xbf, 0xfe, 0x1e, - 0x63, 0x36, 0xcf, 0xce, 0x8b, 0xb0, 0x23, 0x83, 0xa7, 0xc0, 0x4c, 0xfe, 0x3e, 0x56, 0x2d, 0x4c, 0xd4, 0xb0, 0x69, - 0xc9, 0x88, 0x15, 0xf9, 0x5e, 0xc0, 0xab, 0xa5, 0x09, 0xd9, 0xd6, 0x89, 0x55, 0xad, 0xfb, 0xea, 0x26, 0x09, 0xe8, - 0xf5, 0xae, 0xbe, 0x03, 0xd5, 0x55, 0x46, 0x66, 0x40, 0x9f, 0x82, 0xd4, 0x1d, 0xbb, 0xdb, 0x2a, 0xa3, 0xc7, 0x1c, - 0xa1, 0xa7, 0x1c, 0xb5, 0x82, 0x7c, 0x96, 0xf6, 0x7f, 0x3a, 0xea, 0xf4, 0x76, 0x3b, 0x33, 0xe8, 0x0d, 0x72, 0x0b, - 0xde, 0xda, 0xbd, 0xdd, 0x5d, 0x7c, 0x3b, 0x57, 0x6f, 0x5d, 0x7c, 0x8b, 0xd5, 0xdb, 0x03, 0x7c, 0x1b, 0xa9, 0xb7, - 0x87, 0xf8, 0x36, 0x56, 0x6f, 0x8f, 0xf0, 0xed, 0xcc, 0x2e, 0x8f, 0xb8, 0x06, 0xee, 0x11, 0xd0, 0x15, 0x29, 0x89, - 0x81, 0x2a, 0x83, 0xd3, 0x88, 0x37, 0xb0, 0xa2, 0xd3, 0x20, 0xf6, 0x84, 0x02, 0x1d, 0x14, 0xde, 0x39, 0xb0, 0xf4, - 0x80, 0x12, 0x8e, 0x9e, 0xe2, 0x55, 0x7c, 0xd0, 0x3d, 0x0f, 0xe3, 0x19, 0x53, 0xdf, 0x24, 0x55, 0xab, 0x06, 0x35, - 0x05, 0xec, 0xed, 0xb2, 0xa7, 0xf7, 0x49, 0xd8, 0xd0, 0x2a, 0x77, 0x82, 0x76, 0xae, 0xaa, 0x13, 0xd3, 0xb5, 0xf4, - 0x0e, 0x5f, 0x23, 0x20, 0x40, 0x00, 0x2b, 0xa3, 0x74, 0x02, 0x6a, 0x40, 0xeb, 0x02, 0x94, 0xf4, 0xb5, 0x42, 0x03, - 0x21, 0xd2, 0x62, 0x82, 0xd6, 0xa4, 0xdf, 0x0e, 0xa3, 0x53, 0xfd, 0xfc, 0x0a, 0xf4, 0xa9, 0xe8, 0x94, 0xdd, 0x26, - 0x40, 0x08, 0x44, 0x53, 0x78, 0x28, 0x20, 0x48, 0x0b, 0x81, 0xad, 0x41, 0x63, 0x41, 0x0a, 0x0f, 0xc4, 0x4e, 0x5d, - 0x9c, 0xd0, 0xf4, 0xf5, 0x22, 0xc0, 0x68, 0x55, 0xb0, 0x07, 0x6a, 0x1d, 0x95, 0x0a, 0x0c, 0x43, 0x05, 0x16, 0xdc, - 0x28, 0x63, 0x84, 0x2a, 0x72, 0x93, 0xa4, 0xb1, 0x94, 0x90, 0x31, 0x1d, 0xbc, 0xda, 0xbb, 0xbb, 0xca, 0xf7, 0x3e, - 0xeb, 0x8c, 0xf0, 0x8f, 0xe4, 0xaa, 0x9f, 0x4d, 0x26, 0x93, 0x1b, 0x85, 0xce, 0x67, 0xe3, 0x09, 0xeb, 0xb2, 0x07, - 0x3d, 0x74, 0xfe, 0xb5, 0xa4, 0x2f, 0xae, 0x53, 0x12, 0xee, 0x96, 0x77, 0x6b, 0x8c, 0xce, 0x38, 0x90, 0x43, 0x77, - 0x97, 0x4e, 0x25, 0x60, 0x65, 0x09, 0x5c, 0xf9, 0x34, 0x4e, 0x83, 0x76, 0xe9, 0x9f, 0x49, 0x76, 0xfe, 0xd9, 0xe3, - 0xc7, 0x8f, 0x4b, 0x7f, 0xac, 0xde, 0xda, 0xe3, 0x71, 0xe9, 0x8f, 0x96, 0x7a, 0x19, 0xed, 0xf6, 0x64, 0x52, 0xfa, - 0xb1, 0x2a, 0xd8, 0xed, 0x8e, 0xc6, 0xbb, 0xdd, 0xd2, 0x3f, 0x37, 0x5a, 0x94, 0x3e, 0x93, 0x6f, 0x39, 0x1b, 0xd7, - 0x3c, 0x88, 0x8f, 0xc0, 0x78, 0xf5, 0x05, 0xa1, 0x2d, 0xd1, 0x64, 0x10, 0x8f, 0x41, 0xb4, 0xe0, 0x60, 0xeb, 0x02, - 0x6f, 0x67, 0xc0, 0x9f, 0x27, 0x92, 0xb7, 0x8b, 0x4f, 0x7e, 0x22, 0x47, 0xff, 0xd5, 0xe4, 0xe8, 0x48, 0xcc, 0xc4, - 0xcd, 0x19, 0xc9, 0x81, 0x66, 0x35, 0x52, 0x16, 0x55, 0xff, 0x1a, 0xb2, 0x8a, 0xd9, 0x23, 0xb7, 0xc1, 0x96, 0x82, - 0xc7, 0x7f, 0x7d, 0x1d, 0x8f, 0xff, 0xe6, 0x76, 0x1e, 0x7f, 0x72, 0x37, 0x16, 0xff, 0xcd, 0x9f, 0xcc, 0xe2, 0xbf, - 0x6e, 0xb2, 0xf8, 0xcd, 0x3b, 0xb1, 0xf8, 0x35, 0x89, 0x1f, 0xa4, 0x9a, 0xbe, 0x49, 0x43, 0xfb, 0x0d, 0xd8, 0x30, - 0x46, 0xc9, 0x64, 0x02, 0x45, 0x93, 0x89, 0xad, 0x92, 0x1d, 0x81, 0x13, 0x51, 0xab, 0xd7, 0xb5, 0x12, 0x6a, 0xf5, - 0xd5, 0x57, 0x66, 0x99, 0x59, 0x20, 0xfd, 0x0d, 0xa6, 0x7c, 0x57, 0x35, 0x52, 0x65, 0x56, 0x9f, 0x06, 0x19, 0xc7, - 0x05, 0x9e, 0x26, 0x2c, 0x28, 0x79, 0x76, 0x7a, 0x9a, 0x30, 0xfd, 0xed, 0x33, 0xd5, 0xd2, 0x7c, 0x33, 0xe7, 0x33, - 0xcb, 0x07, 0x26, 0xb4, 0x41, 0x0d, 0xd0, 0x9e, 0x70, 0x64, 0xd2, 0xe7, 0xa0, 0x45, 0xd8, 0xfa, 0x4c, 0x7e, 0x37, - 0x98, 0xfc, 0xa9, 0x4b, 0xc9, 0x7e, 0x65, 0x40, 0xb3, 0xea, 0x8a, 0x2e, 0x4c, 0x91, 0x02, 0x32, 0x2e, 0x95, 0xdb, - 0x12, 0xa0, 0x9d, 0xe3, 0x47, 0x4e, 0x74, 0xca, 0xd2, 0xca, 0x37, 0x85, 0x34, 0x9b, 0xc0, 0x8f, 0x1e, 0x88, 0x29, - 0xc4, 0x67, 0x02, 0xf5, 0xb8, 0x22, 0x0e, 0xe8, 0xd4, 0xd6, 0x68, 0xac, 0x2a, 0x0c, 0xcd, 0xa5, 0xa8, 0x9c, 0x93, - 0xd5, 0x79, 0xd6, 0x8a, 0xe6, 0xeb, 0x85, 0xf2, 0xdd, 0xa6, 0xbb, 0x45, 0x34, 0x14, 0xe7, 0x76, 0x5f, 0xdb, 0x98, - 0x35, 0x9a, 0x29, 0xeb, 0x5e, 0x38, 0x9a, 0xe8, 0x24, 0xbb, 0xa8, 0xdb, 0x48, 0x26, 0x0c, 0x68, 0x3e, 0xe9, 0xbd, - 0x57, 0x75, 0xaa, 0xa0, 0x34, 0xbd, 0xa2, 0x22, 0xd3, 0x8b, 0x48, 0x83, 0x7c, 0x60, 0xb0, 0x03, 0xa9, 0x60, 0xca, - 0x30, 0x0f, 0x71, 0x17, 0x6d, 0x47, 0xa0, 0x32, 0x6d, 0x2b, 0x60, 0x51, 0x3a, 0xe4, 0xe8, 0x6b, 0xc2, 0x0e, 0x7d, - 0xab, 0x06, 0x70, 0xaa, 0x6d, 0xb3, 0xdb, 0x19, 0x3e, 0x98, 0x16, 0xe7, 0xc7, 0x7e, 0x71, 0xee, 0xc1, 0x3f, 0xeb, - 0xf3, 0x25, 0xb0, 0xb0, 0x93, 0x4f, 0x31, 0x07, 0x85, 0x71, 0xde, 0x42, 0xa3, 0x98, 0xdc, 0x3b, 0x92, 0xd7, 0x53, - 0xa8, 0x45, 0x5c, 0x89, 0xe8, 0x2d, 0x0a, 0xb4, 0x40, 0x48, 0xd5, 0x0e, 0xd2, 0x2c, 0x65, 0xbd, 0x7a, 0x48, 0xcd, - 0xd4, 0x76, 0x15, 0xb6, 0x86, 0xcb, 0x0c, 0x2d, 0x16, 0x7e, 0x09, 0x16, 0x8b, 0x90, 0x11, 0x6d, 0x15, 0x8e, 0x69, - 0xaf, 0x6d, 0x1f, 0x48, 0x64, 0x6e, 0x93, 0x28, 0xcc, 0x57, 0x55, 0xfa, 0xeb, 0x54, 0xf2, 0xdb, 0x02, 0x4c, 0xdd, - 0x07, 0x0f, 0x3c, 0xf5, 0xcf, 0x88, 0xcc, 0x35, 0x8b, 0x29, 0xc0, 0x74, 0x17, 0xc8, 0x82, 0x68, 0x82, 0x5f, 0x10, - 0xbb, 0x4b, 0xcb, 0x13, 0xca, 0xee, 0x5a, 0xa2, 0xcc, 0x0a, 0x3a, 0x8f, 0xc1, 0xc6, 0xb8, 0xf3, 0xf0, 0x37, 0x2f, - 0xbf, 0x94, 0x38, 0x52, 0x97, 0xf4, 0x6c, 0xbb, 0x87, 0xa7, 0x39, 0x89, 0x2e, 0xc1, 0xd4, 0x21, 0x01, 0x7a, 0x82, - 0xce, 0xae, 0xde, 0x3c, 0x93, 0x91, 0xd2, 0x9c, 0x25, 0xf4, 0x99, 0x7e, 0xb9, 0x15, 0xbb, 0x0f, 0xe7, 0x17, 0x6a, - 0x37, 0x3a, 0x8d, 0x08, 0xe8, 0x9f, 0x1a, 0xe8, 0xbc, 0x3e, 0xb2, 0x5a, 0x0f, 0xd6, 0x3d, 0x00, 0x18, 0x84, 0xd4, - 0x6e, 0xe5, 0x02, 0xaa, 0x36, 0x94, 0x18, 0xa1, 0xde, 0x6a, 0x20, 0xcb, 0xdf, 0x05, 0x09, 0x11, 0x81, 0xbd, 0x8b, - 0x9f, 0x72, 0x8b, 0xc1, 0xa0, 0x92, 0x9a, 0xc1, 0x2c, 0x1e, 0x8f, 0x13, 0xd6, 0x53, 0xc2, 0xdf, 0xea, 0x3c, 0xc4, - 0x48, 0xa9, 0xb9, 0x65, 0xf5, 0x5d, 0x31, 0x90, 0xa7, 0xf1, 0x14, 0x9d, 0x80, 0x32, 0x82, 0xdf, 0x63, 0x5b, 0x8b, - 0x4e, 0x19, 0x42, 0x6c, 0x57, 0xc8, 0xa3, 0xe7, 0xfa, 0x5a, 0x1e, 0x80, 0x26, 0x44, 0x1b, 0x0e, 0x46, 0x75, 0x36, - 0x0f, 0x5a, 0xbb, 0xf5, 0x85, 0x60, 0x95, 0x5e, 0x82, 0xb7, 0x66, 0x59, 0x1e, 0xd0, 0x44, 0x4b, 0x7c, 0xf8, 0xc7, - 0xf2, 0x3b, 0xb2, 0x8c, 0x06, 0xc0, 0x6f, 0x7e, 0xe9, 0xa2, 0xb2, 0xbe, 0x98, 0xff, 0x3f, 0xa7, 0xe5, 0x8b, 0xf5, - 0xa7, 0xe5, 0x0b, 0x75, 0x5a, 0x6e, 0xa6, 0xd8, 0xcf, 0x26, 0x1d, 0xfc, 0xd3, 0xab, 0x16, 0x04, 0xbb, 0x02, 0xe8, - 0xb0, 0x50, 0xe9, 0x6b, 0x75, 0xe1, 0x3f, 0x1a, 0xba, 0xed, 0xe1, 0x1f, 0x1f, 0xd4, 0x9b, 0xb6, 0x85, 0x85, 0xf8, - 0xaf, 0x5d, 0xab, 0xea, 0xdc, 0xc7, 0x3a, 0xec, 0xf5, 0x60, 0xb5, 0xae, 0x7b, 0xf3, 0xa1, 0x05, 0x7e, 0xc5, 0x9d, - 0x40, 0x31, 0x63, 0xb0, 0x43, 0xa2, 0x93, 0x13, 0x28, 0x9d, 0x64, 0xa3, 0x45, 0xf1, 0x8f, 0x12, 0x7e, 0x89, 0xc4, - 0x1b, 0x8f, 0x74, 0x63, 0x1c, 0xd5, 0x55, 0x84, 0xdd, 0xd5, 0x08, 0x4b, 0xbd, 0x4f, 0x41, 0x01, 0x84, 0xc9, 0x9c, - 0xae, 0x7f, 0x7f, 0xcd, 0x21, 0xf8, 0xbb, 0xec, 0xcd, 0xda, 0xc5, 0xfc, 0x7b, 0x91, 0x71, 0x23, 0x12, 0x7e, 0x17, - 0x0e, 0xcc, 0x3d, 0x6c, 0x3f, 0x5e, 0x0f, 0xee, 0x91, 0x9a, 0x69, 0xa8, 0x84, 0x82, 0x94, 0x3b, 0xa0, 0xe2, 0x46, - 0x8b, 0x84, 0xdf, 0x3c, 0xea, 0x75, 0x94, 0xb1, 0x32, 0xea, 0x0d, 0x0c, 0xbd, 0x6a, 0x7b, 0x47, 0x2e, 0xfd, 0xd9, - 0x17, 0xf7, 0xf1, 0x8f, 0xf0, 0xea, 0x9c, 0x54, 0x8a, 0xbf, 0x30, 0x7c, 0x51, 0xf1, 0xdf, 0xac, 0x69, 0xf6, 0x42, - 0x82, 0x93, 0x72, 0x7f, 0xd7, 0xd6, 0xa8, 0xcf, 0xde, 0xa9, 0xb9, 0xd4, 0x83, 0x7e, 0x57, 0xeb, 0xdf, 0x37, 0xf8, - 0x1d, 0xdb, 0x8e, 0x84, 0xce, 0x5c, 0x6f, 0x2b, 0x7f, 0x65, 0xc2, 0x6a, 0x63, 0x81, 0xe7, 0xbb, 0x36, 0x57, 0x1b, - 0x44, 0xed, 0x37, 0xc3, 0x13, 0x6d, 0x1e, 0xc9, 0xb0, 0x1b, 0xb6, 0x17, 0x16, 0xd2, 0xb7, 0x2c, 0xbc, 0x87, 0x9f, - 0x1a, 0xb2, 0x2e, 0x66, 0x49, 0x0a, 0x3a, 0xd5, 0x94, 0xf3, 0x79, 0xb0, 0xb3, 0x73, 0x7e, 0x7e, 0xee, 0x9f, 0xef, - 0xfa, 0x59, 0x7e, 0xba, 0xd3, 0x6d, 0xb7, 0xdb, 0xf8, 0x85, 0x18, 0xdb, 0x3a, 0x8b, 0xd9, 0xf9, 0x97, 0xd9, 0x45, - 0x68, 0x3f, 0xb2, 0x1e, 0x5b, 0x8f, 0x76, 0xad, 0x07, 0x0f, 0x6d, 0x8b, 0xb8, 0x3f, 0x94, 0xec, 0xda, 0x96, 0xe0, - 0xfe, 0xa1, 0x0d, 0xc5, 0xfd, 0xbd, 0x53, 0xa5, 0xc0, 0x61, 0x06, 0xae, 0x50, 0x8f, 0xc0, 0x66, 0xc9, 0x3e, 0xb1, - 0xfa, 0x39, 0x17, 0x65, 0x2d, 0x29, 0x43, 0xd4, 0x2b, 0x1e, 0xf6, 0x51, 0x34, 0x0f, 0x88, 0x86, 0xcc, 0x42, 0x74, - 0x00, 0x89, 0x52, 0x9a, 0x02, 0xa3, 0xba, 0x27, 0xf0, 0x04, 0x1a, 0xfb, 0xd4, 0x82, 0xe7, 0x57, 0xdd, 0x47, 0x20, - 0xe0, 0xce, 0x5a, 0xf7, 0x47, 0xed, 0x56, 0xc7, 0xea, 0xb4, 0xba, 0xfe, 0x23, 0xab, 0x2b, 0xfe, 0x07, 0x06, 0xb9, - 0x6b, 0x75, 0xe0, 0x69, 0xd7, 0x82, 0xf7, 0xb3, 0xfb, 0x22, 0x24, 0x1c, 0xd9, 0x3b, 0xfd, 0x3d, 0xfc, 0x85, 0x29, - 0xb0, 0xa8, 0x2f, 0x6c, 0xf1, 0x2b, 0x9e, 0xec, 0xcf, 0xcc, 0xd2, 0xce, 0xe3, 0xb5, 0xc5, 0xdd, 0x47, 0x6b, 0x8b, - 0x77, 0x1f, 0xae, 0x2d, 0xbe, 0xff, 0xa0, 0x5e, 0xbc, 0x73, 0x2a, 0xaa, 0x34, 0x53, 0x08, 0xed, 0x59, 0x04, 0x54, - 0x72, 0xe1, 0x74, 0x00, 0xce, 0xb6, 0xd5, 0xc2, 0x1f, 0x8f, 0xba, 0xae, 0xee, 0x75, 0x82, 0xbd, 0xf4, 0x2a, 0x1f, - 0x3d, 0x86, 0x55, 0x3e, 0xef, 0x3e, 0x1c, 0x61, 0x3b, 0x5a, 0x28, 0xfc, 0x3b, 0xdb, 0x7d, 0x3c, 0x02, 0x71, 0x60, - 0xe1, 0x3f, 0xf8, 0x33, 0x7d, 0xd0, 0x1d, 0x89, 0x97, 0x36, 0xd6, 0x7f, 0xe8, 0x3c, 0x2a, 0xa0, 0x29, 0xfe, 0xf9, - 0x4d, 0xeb, 0xcf, 0xa8, 0xbe, 0x9b, 0xe3, 0xde, 0x07, 0x1c, 0x3d, 0x9e, 0x76, 0xfd, 0x2f, 0xce, 0x1e, 0xf9, 0x8f, - 0xa7, 0x9d, 0x47, 0x1f, 0xc4, 0x5b, 0x02, 0x18, 0xfc, 0x02, 0xff, 0x7d, 0xd8, 0x6d, 0x83, 0x69, 0xeb, 0x3f, 0x3e, - 0xdb, 0xf5, 0x77, 0x93, 0xd6, 0x43, 0xff, 0x31, 0xfe, 0xab, 0x86, 0x9b, 0x66, 0x33, 0x66, 0x5b, 0xb8, 0xdf, 0x0d, - 0xbb, 0xd0, 0x9c, 0xa3, 0x7b, 0xdf, 0x7a, 0x70, 0xff, 0xf9, 0x63, 0xd8, 0xa3, 0x69, 0xa7, 0x0b, 0xff, 0x5f, 0xf7, - 0xf8, 0x01, 0x11, 0x2f, 0x07, 0x8e, 0x18, 0xe6, 0xce, 0x29, 0xc4, 0xd1, 0xd7, 0x8a, 0xee, 0x79, 0x3f, 0x5e, 0x67, - 0xda, 0xff, 0x70, 0xbb, 0x69, 0xff, 0xd7, 0x3b, 0xba, 0x6f, 0x7f, 0xf8, 0x93, 0x6d, 0xfb, 0x1f, 0x9b, 0xb6, 0xfd, - 0x39, 0x5b, 0x31, 0xee, 0x9b, 0xf6, 0xd9, 0x21, 0x73, 0x8e, 0xbe, 0x65, 0x43, 0xcc, 0x13, 0x85, 0xd6, 0x7f, 0xad, - 0x79, 0x3a, 0x32, 0x3c, 0xc8, 0xe7, 0x4c, 0x9c, 0xe4, 0xef, 0xaf, 0x43, 0x08, 0xe3, 0xb7, 0x22, 0xe4, 0xc5, 0xdd, - 0xf0, 0x41, 0x9f, 0x16, 0xff, 0x13, 0xf1, 0xf1, 0xbd, 0x89, 0x8f, 0x9a, 0x2f, 0x99, 0x8c, 0x79, 0xb2, 0xc1, 0x0f, - 0xe8, 0xdd, 0xb1, 0x77, 0x18, 0xbe, 0x15, 0xb6, 0x48, 0x7e, 0x7a, 0xf7, 0x7b, 0xfc, 0xde, 0x22, 0x8d, 0x32, 0xb4, - 0xa5, 0x83, 0x62, 0x8e, 0x1f, 0xe3, 0x54, 0x2f, 0x67, 0x22, 0x55, 0x3f, 0xa4, 0x7b, 0x36, 0xf7, 0xb5, 0x73, 0x03, - 0x33, 0x5b, 0xc3, 0x65, 0xc6, 0x23, 0xfc, 0x6d, 0x2f, 0x3c, 0xe6, 0x09, 0xde, 0x02, 0x94, 0x37, 0x66, 0x30, 0x11, - 0xf3, 0x5b, 0x4c, 0x22, 0x55, 0xee, 0xef, 0x19, 0x3a, 0x0c, 0x5e, 0xb1, 0x71, 0x1c, 0x39, 0xb6, 0x33, 0x87, 0x13, - 0x0b, 0x63, 0xb6, 0x6a, 0x19, 0x9c, 0x94, 0xbc, 0xe9, 0xda, 0xea, 0x17, 0x8c, 0xe4, 0xf8, 0xc1, 0xa6, 0xf0, 0x48, - 0xba, 0xce, 0x6c, 0xa9, 0xfe, 0xc3, 0xf8, 0xaa, 0x24, 0x47, 0xd6, 0x5d, 0xa9, 0x0c, 0xb6, 0xd0, 0x19, 0x3a, 0x7e, - 0x17, 0x6c, 0x08, 0x2a, 0xc6, 0x0f, 0xe0, 0xfc, 0xe0, 0xb4, 0x76, 0x41, 0xa7, 0x31, 0xba, 0xe9, 0x81, 0x86, 0x2b, - 0x1f, 0xdf, 0x14, 0x7e, 0x83, 0x46, 0xa9, 0xa7, 0x7f, 0xe3, 0x12, 0x50, 0x86, 0xca, 0xf5, 0xff, 0xf2, 0xf2, 0x50, - 0x5e, 0x72, 0xb5, 0xd1, 0x27, 0x49, 0xbe, 0xe8, 0xea, 0x03, 0x3b, 0xdb, 0x20, 0x2e, 0xe8, 0xd7, 0xde, 0x51, 0x50, - 0x16, 0x25, 0x02, 0xe6, 0x98, 0x5a, 0xd2, 0x6c, 0x08, 0x6d, 0x21, 0x0f, 0xc6, 0xec, 0x2c, 0x1e, 0x49, 0xb6, 0xee, - 0x59, 0x32, 0x37, 0xbe, 0x45, 0xab, 0x08, 0x3b, 0x9e, 0x30, 0x9c, 0xe1, 0x05, 0x65, 0x54, 0x98, 0x66, 0x76, 0xff, - 0x5e, 0x4f, 0x43, 0x52, 0x4f, 0xcf, 0xb5, 0xf1, 0x77, 0xf0, 0x1d, 0x81, 0xa1, 0xf6, 0x8f, 0xe1, 0x3d, 0xfc, 0x2d, - 0x7c, 0xf7, 0x86, 0xb6, 0xeb, 0x13, 0x53, 0xbc, 0x57, 0xfd, 0x2a, 0x3e, 0xe4, 0x08, 0xdb, 0x20, 0xbf, 0xbc, 0xbb, - 0x0a, 0x32, 0x29, 0xb4, 0xba, 0x0f, 0x2a, 0xa1, 0x05, 0xcf, 0x06, 0x97, 0x02, 0x06, 0xda, 0xf5, 0x1f, 0x18, 0xac, - 0xf0, 0xac, 0x85, 0x3f, 0x6b, 0xcc, 0xf0, 0x3e, 0x34, 0x50, 0xdc, 0xf0, 0x25, 0x34, 0xdf, 0x15, 0x8c, 0x17, 0xfa, - 0xfd, 0x48, 0xac, 0x4a, 0xb0, 0xa9, 0x3a, 0xc5, 0xac, 0x09, 0x8f, 0x88, 0x78, 0xb6, 0xed, 0x39, 0xfa, 0xfb, 0xfe, - 0x92, 0x5c, 0xe5, 0xe5, 0xa4, 0xa7, 0xd0, 0xd7, 0xd1, 0xdf, 0xad, 0x5d, 0x57, 0xe7, 0xd5, 0x4e, 0xce, 0x9a, 0x29, - 0x90, 0xe0, 0x1b, 0x21, 0x18, 0xca, 0xd5, 0x16, 0xdf, 0x6f, 0x12, 0xc7, 0xb8, 0xfa, 0xc2, 0xd5, 0x9a, 0x74, 0x43, - 0xf3, 0x50, 0xb0, 0x8a, 0x68, 0xe8, 0x5c, 0x00, 0x23, 0xa0, 0x9f, 0x55, 0xb1, 0x7a, 0x90, 0x04, 0xe5, 0x27, 0x11, - 0xfe, 0xfa, 0x09, 0xfa, 0x51, 0x56, 0x07, 0x90, 0xd3, 0x07, 0xfa, 0x08, 0xd2, 0x17, 0xe3, 0xb2, 0xb9, 0x08, 0xd0, - 0x17, 0xf0, 0xb7, 0x99, 0x55, 0xb9, 0xe1, 0xf2, 0xd2, 0x17, 0x86, 0xc1, 0xc7, 0x71, 0x4e, 0x77, 0x09, 0xd5, 0xfa, - 0x6b, 0xd7, 0xfc, 0x2a, 0x54, 0xd3, 0xa9, 0x64, 0xc5, 0xc0, 0xc6, 0x22, 0x5b, 0x65, 0xe9, 0x98, 0x5f, 0xa8, 0x35, - 0x2f, 0x7b, 0x8d, 0x45, 0x9a, 0x0e, 0x7e, 0xc1, 0xdb, 0x16, 0x48, 0xb6, 0x81, 0x8d, 0x5d, 0xbb, 0x26, 0x52, 0x6e, - 0xf0, 0x8e, 0x54, 0xf5, 0x2b, 0x59, 0xcc, 0x03, 0x6f, 0x9b, 0xbb, 0xa5, 0xc7, 0xa5, 0x7d, 0x70, 0xa5, 0xa7, 0xf0, - 0x84, 0x45, 0xdc, 0x8f, 0x52, 0xca, 0xf7, 0x70, 0x0c, 0xb6, 0xe0, 0x75, 0xd8, 0xae, 0x5b, 0x02, 0xe7, 0x31, 0x7e, - 0x67, 0x8d, 0x40, 0xbd, 0x0f, 0x85, 0x6e, 0xe5, 0xb5, 0x9b, 0x76, 0xfb, 0x6f, 0x0e, 0xf7, 0x2d, 0x71, 0x9a, 0xf7, - 0x76, 0xe0, 0x75, 0x8f, 0x6c, 0x61, 0x91, 0x52, 0x10, 0x8a, 0x94, 0x02, 0x4b, 0x64, 0xc3, 0x84, 0xf6, 0x8e, 0x58, - 0xa6, 0x6d, 0xb1, 0x74, 0x24, 0x3c, 0x78, 0x33, 0xb0, 0x15, 0x62, 0xfc, 0x8a, 0xd1, 0x0e, 0x76, 0x6b, 0xe1, 0x4e, - 0xc3, 0x11, 0x10, 0x3e, 0x3e, 0xa5, 0x20, 0xf0, 0xd4, 0x96, 0xfe, 0x3e, 0x10, 0xeb, 0x4c, 0x65, 0x62, 0xc8, 0xa1, - 0x74, 0x5e, 0xde, 0x6a, 0xeb, 0x62, 0x71, 0x32, 0x03, 0x3e, 0xa4, 0x92, 0x29, 0xde, 0xcb, 0x0e, 0x7b, 0x34, 0x15, - 0x66, 0x01, 0xae, 0x3a, 0x21, 0xa7, 0x9d, 0xfe, 0x5e, 0x24, 0xf5, 0x1d, 0x3c, 0xbb, 0x05, 0x1c, 0x5e, 0x10, 0x73, - 0xa8, 0x54, 0xf8, 0x71, 0xb6, 0x73, 0xce, 0x4e, 0x5a, 0xd1, 0x3c, 0xae, 0x7c, 0x7f, 0x28, 0xfd, 0xfa, 0x7b, 0x4a, - 0x10, 0xca, 0x84, 0x33, 0xf9, 0x18, 0x19, 0x89, 0x07, 0x88, 0x38, 0x22, 0xd0, 0x52, 0x3a, 0x16, 0x49, 0x69, 0x04, - 0xe4, 0x03, 0xac, 0x44, 0xbf, 0xca, 0x01, 0x29, 0x25, 0x41, 0x69, 0xf7, 0xff, 0xf6, 0xbf, 0xfe, 0xb7, 0xf4, 0x29, - 0x02, 0x5a, 0x01, 0x2c, 0xcc, 0xdc, 0xa8, 0x62, 0x67, 0xec, 0x02, 0xac, 0xd0, 0x78, 0xdc, 0x9a, 0x46, 0xc9, 0x04, - 0x20, 0x28, 0x98, 0xb8, 0xbb, 0x21, 0xeb, 0x81, 0x0a, 0x24, 0x58, 0x66, 0xd8, 0x59, 0x82, 0x57, 0x2f, 0xc2, 0x1d, - 0xfb, 0x43, 0x19, 0x7c, 0x2a, 0xb7, 0x94, 0x08, 0xda, 0xc8, 0xe7, 0x33, 0x68, 0xae, 0x96, 0xd3, 0xa7, 0x7e, 0x23, - 0x8c, 0x64, 0x1e, 0xac, 0x96, 0xd0, 0x07, 0x2d, 0x75, 0xa0, 0xe0, 0xdf, 0xfe, 0xf5, 0x3f, 0xff, 0x77, 0xf5, 0x8a, - 0xfe, 0xff, 0xbf, 0xfd, 0xcb, 0x3f, 0xfd, 0xdf, 0xff, 0xf3, 0x5f, 0x30, 0x39, 0x52, 0xc6, 0x08, 0xe8, 0x28, 0x59, - 0x55, 0x80, 0x40, 0x9c, 0xa9, 0x7a, 0xb6, 0xdf, 0x01, 0xcd, 0x42, 0x04, 0x29, 0x41, 0x22, 0x62, 0xa6, 0x24, 0x50, - 0x42, 0xd5, 0x0d, 0x38, 0x83, 0xfd, 0xb3, 0x28, 0x4a, 0x6d, 0x3f, 0x68, 0xdb, 0xd5, 0x9e, 0xf6, 0x8d, 0xbe, 0x3b, - 0xb8, 0x1b, 0x77, 0xca, 0x14, 0xf1, 0xf5, 0x5e, 0x2d, 0x95, 0xe3, 0x0a, 0x4b, 0xca, 0xaa, 0xdc, 0x42, 0x8f, 0xf2, - 0x12, 0x5f, 0x83, 0xae, 0x51, 0x4c, 0x5b, 0x5b, 0xeb, 0xd3, 0xfb, 0x65, 0x51, 0xf0, 0x78, 0x82, 0xfb, 0x21, 0xdc, - 0x63, 0x14, 0x0a, 0x6c, 0xa1, 0x4a, 0x92, 0x5c, 0x96, 0x34, 0x8a, 0x30, 0x61, 0xee, 0x3f, 0xfe, 0x87, 0xf2, 0x2f, - 0x33, 0x54, 0x05, 0x2c, 0x67, 0x16, 0x5d, 0x48, 0xc3, 0xe6, 0x61, 0xbb, 0x3d, 0xbf, 0x70, 0x97, 0xd5, 0x0c, 0xde, - 0x75, 0x93, 0x91, 0x4b, 0xcd, 0x1c, 0x90, 0x62, 0x88, 0xda, 0x7b, 0x07, 0xba, 0x7c, 0x1b, 0x9d, 0x3d, 0x65, 0xf9, - 0xf9, 0x92, 0x1c, 0x48, 0xf1, 0x6f, 0x18, 0xeb, 0x93, 0xbe, 0x36, 0x28, 0x31, 0x56, 0xb1, 0x34, 0x7a, 0x75, 0x45, - 0xaf, 0x69, 0x67, 0x35, 0xd3, 0xc4, 0x8c, 0x55, 0x9a, 0x51, 0x46, 0xcc, 0xc3, 0x80, 0x0e, 0xde, 0xb4, 0xbb, 0xd4, - 0xc3, 0x73, 0x9e, 0xcd, 0xcc, 0xe0, 0x24, 0x8b, 0xd8, 0x88, 0x4d, 0x94, 0x8f, 0x52, 0xd6, 0x8b, 0xc0, 0x63, 0xf9, - 0x19, 0x9e, 0x31, 0xc0, 0x6d, 0x16, 0xf1, 0x80, 0x28, 0xb5, 0x67, 0x86, 0x2f, 0x23, 0x0c, 0x0c, 0x67, 0x4b, 0x63, - 0xae, 0x9e, 0x68, 0x8a, 0x9e, 0xc0, 0x7a, 0x7e, 0x4a, 0xe9, 0x53, 0x77, 0x73, 0x28, 0xe1, 0x48, 0x78, 0x51, 0x65, - 0x87, 0x54, 0x26, 0xf6, 0xbb, 0x9a, 0x39, 0x2e, 0x99, 0x31, 0x18, 0xc1, 0xb7, 0x37, 0x16, 0x52, 0x52, 0x34, 0xfd, - 0x15, 0x94, 0x1f, 0x5a, 0x80, 0xdd, 0x6c, 0x45, 0x85, 0xd8, 0xea, 0x5d, 0xf8, 0x42, 0xab, 0xe2, 0xd1, 0x7c, 0x4e, - 0x0d, 0x5d, 0xa0, 0x53, 0x52, 0xa9, 0x91, 0x71, 0x50, 0x2c, 0x5c, 0x84, 0x9e, 0x65, 0x1b, 0x49, 0xd0, 0xe2, 0x49, - 0x06, 0xa5, 0xe9, 0xf7, 0x0d, 0xff, 0x3f, 0xdf, 0x8d, 0x21, 0x2b, 0x85, 0x78, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xbd, 0x7d, 0xd9, 0x76, 0xdb, 0xc8, 0x92, 0xe0, 0xf3, + 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x30, 0x4a, 0x6d, 0x03, 0x25, 0x10, 0x22, 0x29, 0xcb, 0x76, 0x81, 0x02, 0x79, 0xe5, + 0xa5, 0xae, 0x5d, 0xe5, 0xad, 0x2c, 0xd9, 0x75, 0xab, 0x54, 0x2c, 0x0b, 0x22, 0x93, 0x22, 0xca, 0x20, 0xc0, 0x02, + 0x92, 0x5a, 0x8a, 0x42, 0x9f, 0x7e, 0xea, 0xa7, 0x39, 0x67, 0xd6, 0x87, 0x7e, 0x99, 0xd3, 0xfd, 0x30, 0x1f, 0x31, + 0xcf, 0xfd, 0x29, 0xf7, 0x07, 0xa6, 0x3f, 0x61, 0x22, 0x22, 0x17, 0x24, 0x40, 0x6a, 0x71, 0x75, 0xcd, 0x3d, 0x5e, + 0x04, 0xe4, 0x1a, 0x11, 0x19, 0x19, 0x5b, 0x46, 0x42, 0xbb, 0x77, 0xc6, 0xd9, 0x88, 0x5f, 0xcc, 0x99, 0x35, 0xe5, + 0xb3, 0xa4, 0xbf, 0x2b, 0xff, 0x67, 0xd1, 0xb8, 0xbf, 0x9b, 0xc4, 0xe9, 0x27, 0x2b, 0x67, 0x49, 0x18, 0x8f, 0xb2, + 0xd4, 0x9a, 0xe6, 0x6c, 0x12, 0x8e, 0x23, 0x1e, 0x05, 0xf1, 0x2c, 0x3a, 0x61, 0xd6, 0x56, 0x7f, 0x77, 0xc6, 0x78, + 0x64, 0x8d, 0xa6, 0x51, 0x5e, 0x30, 0x1e, 0xbe, 0x3f, 0xf8, 0xba, 0xf5, 0xa8, 0xbf, 0x5b, 0x8c, 0xf2, 0x78, 0xce, + 0x2d, 0x1c, 0x32, 0x9c, 0x65, 0xe3, 0x45, 0xc2, 0xfa, 0xa7, 0x51, 0x6e, 0x9d, 0xb3, 0xf0, 0xcd, 0xf1, 0x2f, 0x6c, + 0xc4, 0xfd, 0x31, 0x9b, 0xc4, 0x29, 0x7b, 0x9b, 0x67, 0x73, 0x96, 0xf3, 0x0b, 0xef, 0xd9, 0xfa, 0x8a, 0x98, 0x15, + 0xde, 0xbe, 0xae, 0x3a, 0x61, 0xfc, 0xcd, 0x59, 0xaa, 0xfa, 0x3c, 0x65, 0x62, 0x92, 0x2c, 0x2f, 0x3c, 0x7e, 0x45, + 0x9b, 0xfd, 0x8b, 0xd9, 0x71, 0x96, 0x14, 0xde, 0x27, 0x5d, 0x3f, 0xcf, 0x33, 0x9e, 0x21, 0x58, 0xfe, 0x34, 0x2a, + 0x8c, 0x96, 0xde, 0x93, 0x35, 0x4d, 0xe6, 0xb2, 0xf2, 0x45, 0xf1, 0x2c, 0x5d, 0xcc, 0x58, 0x1e, 0x1d, 0x27, 0xcc, + 0x2b, 0x58, 0xe8, 0x30, 0x8f, 0x7b, 0xb1, 0x1b, 0xf6, 0xb9, 0x15, 0xa7, 0x16, 0x1b, 0x9c, 0x33, 0x2a, 0x59, 0x32, + 0xdd, 0x2a, 0xb8, 0xd3, 0xf6, 0x80, 0x5c, 0x93, 0xf8, 0x64, 0xa1, 0xdf, 0xcf, 0xf2, 0x98, 0xab, 0xe7, 0xd3, 0x28, + 0x59, 0xb0, 0x20, 0x2e, 0xdd, 0x80, 0x1d, 0xf2, 0x61, 0x18, 0x7b, 0x4f, 0x68, 0x50, 0x18, 0x72, 0x39, 0xc9, 0x72, + 0x07, 0x69, 0x15, 0xe3, 0xd8, 0xfc, 0xf2, 0xd2, 0xe1, 0xe1, 0xb2, 0x74, 0xdd, 0x4f, 0xcc, 0x1f, 0x45, 0x49, 0xe2, + 0xe0, 0xc4, 0x77, 0xef, 0x16, 0x38, 0x63, 0xec, 0xf1, 0xc3, 0x78, 0xe8, 0xf6, 0xe2, 0x89, 0xc3, 0x99, 0x5b, 0xf5, + 0xcb, 0x26, 0x16, 0x67, 0x0e, 0x77, 0xdd, 0x27, 0x57, 0xf7, 0xc9, 0x19, 0x5f, 0xe4, 0x00, 0x7b, 0xe9, 0xbd, 0x51, + 0x33, 0x3f, 0xc3, 0xfa, 0x7d, 0xea, 0xd8, 0x03, 0xd8, 0x0b, 0x6e, 0x7d, 0x08, 0xcf, 0xe2, 0x74, 0x9c, 0x9d, 0xf9, + 0xfb, 0xd3, 0x08, 0x7e, 0xbc, 0xcb, 0x32, 0x7e, 0xf7, 0xae, 0x73, 0x9a, 0xc5, 0x63, 0xab, 0x1d, 0x86, 0x66, 0xe5, + 0xc5, 0x93, 0xfd, 0xfd, 0xcb, 0xcb, 0x46, 0x81, 0x9f, 0x46, 0x3c, 0x3e, 0x65, 0xa2, 0x33, 0x00, 0x60, 0xc3, 0xcf, + 0x39, 0x67, 0xe3, 0x7d, 0x7e, 0x91, 0x40, 0x29, 0x63, 0xbc, 0xb0, 0x01, 0xc7, 0xa7, 0xd9, 0x08, 0xc8, 0x96, 0x1a, + 0x84, 0x87, 0xa6, 0x39, 0x9b, 0x27, 0xd1, 0x88, 0x61, 0x3d, 0x8c, 0x54, 0xf5, 0xa8, 0x1a, 0x79, 0x5f, 0x87, 0x62, + 0x79, 0x1d, 0xd7, 0x8b, 0x59, 0x98, 0xb2, 0x33, 0xeb, 0x55, 0x34, 0xef, 0x8d, 0x92, 0xa8, 0x28, 0x80, 0x5f, 0x97, + 0x84, 0x42, 0xbe, 0x18, 0x01, 0x83, 0x10, 0x82, 0x4b, 0x24, 0xd3, 0x34, 0x2e, 0xfc, 0x8f, 0x1b, 0xa3, 0xa2, 0x78, + 0xc7, 0x8a, 0x45, 0xc2, 0x37, 0x42, 0x58, 0x0b, 0x7e, 0x27, 0x0c, 0xbf, 0x76, 0xf9, 0x34, 0xcf, 0xce, 0xac, 0x67, + 0x79, 0x0e, 0xcd, 0x6d, 0x98, 0x52, 0x34, 0xb0, 0xe2, 0xc2, 0x4a, 0x33, 0x6e, 0xe9, 0xc1, 0x70, 0x01, 0x7d, 0xeb, + 0x7d, 0xc1, 0xac, 0xa3, 0x45, 0x5a, 0x44, 0x13, 0x06, 0x4d, 0x8f, 0xac, 0x2c, 0xb7, 0x8e, 0x60, 0xd0, 0x23, 0x58, + 0xb2, 0x82, 0xc3, 0xae, 0xf1, 0x6d, 0xb7, 0x47, 0x73, 0x41, 0xe1, 0x01, 0x3b, 0xe7, 0x21, 0x2b, 0x81, 0x31, 0xad, + 0x42, 0xa3, 0xe1, 0xb8, 0xcb, 0x04, 0x0a, 0x58, 0x18, 0x33, 0x64, 0x59, 0xc7, 0x6c, 0xac, 0x17, 0xe7, 0xc3, 0xdd, + 0xbb, 0x9a, 0xd6, 0x40, 0x13, 0x07, 0xda, 0x16, 0x8d, 0xb6, 0x9e, 0x40, 0xbc, 0x46, 0x22, 0xd7, 0x63, 0xbe, 0x24, + 0xdf, 0xfe, 0x45, 0x3a, 0xaa, 0x8f, 0x0d, 0x95, 0x25, 0xcf, 0xf6, 0x79, 0x1e, 0xa7, 0x27, 0x00, 0x84, 0x9c, 0xc9, + 0x6c, 0x52, 0x96, 0x62, 0xf1, 0xdf, 0xb0, 0x90, 0x85, 0x7d, 0x1c, 0x3d, 0x67, 0x8e, 0x5d, 0x50, 0x0f, 0x3b, 0x0c, + 0x91, 0xf4, 0xc0, 0x60, 0x6c, 0xc0, 0x02, 0xb6, 0x69, 0xdb, 0xde, 0xd7, 0xae, 0x77, 0x81, 0x1c, 0xe4, 0xfb, 0x3e, + 0xb1, 0xaf, 0xe8, 0x1c, 0x87, 0x1d, 0x04, 0xda, 0x4f, 0x58, 0x7a, 0xc2, 0xa7, 0x03, 0x76, 0xd8, 0x1e, 0x06, 0x1c, + 0xa0, 0x1a, 0x2f, 0x46, 0xcc, 0x41, 0x7e, 0xf4, 0x72, 0xdc, 0x3e, 0x9b, 0x0e, 0x4c, 0x81, 0x0b, 0x73, 0x87, 0x70, + 0xac, 0x2d, 0x8d, 0xab, 0x58, 0x54, 0x01, 0x86, 0x7c, 0x6e, 0xc3, 0x0e, 0x3b, 0x66, 0xb9, 0x01, 0x87, 0x6e, 0xd6, + 0xab, 0xad, 0xe0, 0x02, 0x56, 0x08, 0xfa, 0x59, 0x93, 0x45, 0x3a, 0xe2, 0x31, 0x08, 0x2e, 0x7b, 0x13, 0xc0, 0x15, + 0x2b, 0xa7, 0x17, 0xce, 0x76, 0x4b, 0xd7, 0x89, 0xdd, 0x4d, 0x76, 0x98, 0x6f, 0x76, 0x86, 0x1e, 0x42, 0xa9, 0x89, + 0x2f, 0x11, 0x8f, 0x01, 0xc1, 0xd2, 0x7b, 0xcb, 0xf4, 0xf6, 0xfc, 0x30, 0x60, 0xfe, 0x2a, 0x1f, 0x87, 0xdc, 0x9f, + 0x45, 0x73, 0xc4, 0x86, 0x11, 0x0f, 0x44, 0xe9, 0x08, 0xa1, 0xab, 0xad, 0x0b, 0x52, 0xcc, 0xaf, 0x58, 0xc0, 0x05, + 0x82, 0xc0, 0x9e, 0x7d, 0x16, 0x8d, 0xa6, 0xb0, 0xc5, 0x2b, 0xc2, 0x8d, 0xd5, 0x76, 0x18, 0xe5, 0x2c, 0xe2, 0xec, + 0x59, 0xc2, 0xf0, 0x0d, 0x57, 0x00, 0x7a, 0xda, 0xae, 0x97, 0xab, 0x7d, 0x97, 0xc4, 0xfc, 0x75, 0x06, 0xf3, 0xf4, + 0x04, 0x93, 0x00, 0x17, 0xe7, 0x77, 0xef, 0xc6, 0xc8, 0x22, 0x7b, 0x1c, 0x56, 0xeb, 0x78, 0x01, 0x42, 0xc0, 0x4e, + 0xb1, 0x85, 0x0d, 0xd4, 0xf6, 0x62, 0x9f, 0x03, 0x11, 0x9f, 0x64, 0x29, 0x87, 0xe1, 0x00, 0x5e, 0xcd, 0x41, 0x7e, + 0x34, 0x9f, 0xb3, 0x74, 0xfc, 0x64, 0x1a, 0x27, 0x63, 0xa0, 0x46, 0x09, 0xf8, 0x66, 0x2c, 0x04, 0x3c, 0x01, 0x99, + 0xe0, 0x7a, 0x8c, 0x68, 0xf9, 0x90, 0x91, 0x79, 0x68, 0xdb, 0x3d, 0x94, 0x40, 0x12, 0x0b, 0x94, 0x41, 0xb4, 0x70, + 0xef, 0x40, 0xf4, 0x17, 0x2e, 0xdf, 0x0c, 0x63, 0xbd, 0x8c, 0x92, 0xc0, 0x6f, 0x50, 0xd2, 0x00, 0xfd, 0x19, 0xc8, + 0xc0, 0x1e, 0x0a, 0xae, 0xef, 0xa5, 0xd4, 0x49, 0x99, 0xc2, 0x10, 0x08, 0x30, 0x42, 0x09, 0x22, 0x69, 0xf0, 0x36, + 0x4b, 0x2e, 0x26, 0x71, 0x92, 0xec, 0x2f, 0xe6, 0xf3, 0x2c, 0xe7, 0xde, 0x37, 0xe1, 0x92, 0x67, 0x15, 0xae, 0xb4, + 0xc9, 0x8b, 0xb3, 0x98, 0x23, 0x41, 0xdd, 0xe5, 0x28, 0x82, 0xa5, 0x7e, 0x9c, 0x65, 0x09, 0x8b, 0x52, 0x40, 0x83, + 0x0d, 0x6c, 0x3b, 0x48, 0x17, 0x49, 0xd2, 0x3b, 0x86, 0x61, 0x3f, 0xf5, 0xa8, 0x5a, 0x48, 0xfc, 0x80, 0x9e, 0xf7, + 0xf2, 0x3c, 0xba, 0x80, 0x86, 0xd8, 0x06, 0x78, 0x11, 0x56, 0xeb, 0x9b, 0xfd, 0x37, 0xaf, 0x7d, 0xc1, 0xf8, 0xf1, + 0xe4, 0x02, 0x00, 0x2d, 0x2b, 0xa9, 0x39, 0xc9, 0xb3, 0x59, 0x63, 0x6a, 0xa4, 0x43, 0x1c, 0xb2, 0xde, 0x15, 0x20, + 0xc4, 0x34, 0x32, 0xac, 0x12, 0x33, 0x21, 0x78, 0x4d, 0xfc, 0x2c, 0x2b, 0x71, 0x0f, 0x0c, 0xf0, 0x21, 0x10, 0xc5, + 0x30, 0xe5, 0xf5, 0xd0, 0xf2, 0xfc, 0x62, 0x19, 0x87, 0x04, 0xe7, 0x1c, 0xf5, 0x2f, 0xc2, 0x38, 0x8a, 0x60, 0xf6, + 0xa5, 0x18, 0xb0, 0x54, 0x10, 0xc7, 0x65, 0xe9, 0x25, 0x9a, 0x89, 0x51, 0xe2, 0xa1, 0x40, 0xe1, 0xb0, 0x8d, 0x2e, + 0x2f, 0x19, 0xbc, 0xb8, 0xde, 0xb7, 0xe1, 0x32, 0x52, 0xf8, 0xa0, 0x86, 0xc2, 0xfd, 0x15, 0x08, 0x39, 0x81, 0x9a, + 0xec, 0x14, 0xf4, 0x20, 0xc0, 0xf9, 0x8d, 0x07, 0xfa, 0x3f, 0x41, 0x28, 0xee, 0x74, 0x3c, 0xd0, 0xa0, 0x4f, 0xa6, + 0x51, 0x7a, 0xc2, 0xc6, 0x41, 0xc2, 0x4a, 0x29, 0x79, 0xf7, 0x2c, 0x58, 0x63, 0x60, 0xa7, 0xc2, 0x7a, 0x7e, 0xf0, + 0xea, 0xa5, 0x5c, 0xb9, 0x9a, 0x30, 0x86, 0x45, 0x5a, 0x80, 0x5a, 0x05, 0xb1, 0x2d, 0xc5, 0xf1, 0x33, 0xae, 0xa4, + 0xb7, 0x28, 0x89, 0x8b, 0xf7, 0x73, 0x30, 0x31, 0xd8, 0x5b, 0x18, 0x06, 0xa6, 0x0f, 0x61, 0x2a, 0x2a, 0x87, 0xf9, + 0x44, 0xc5, 0x58, 0x17, 0x41, 0x67, 0x81, 0xa9, 0x78, 0xcd, 0x1c, 0xb7, 0x04, 0x56, 0xe5, 0xf1, 0xc8, 0x8a, 0xc6, + 0xe3, 0x17, 0x69, 0xcc, 0xe3, 0x28, 0x89, 0x7f, 0x23, 0x4a, 0x2e, 0x91, 0xc7, 0x78, 0x4f, 0x2e, 0x02, 0xe0, 0x4e, + 0x3d, 0x12, 0x57, 0x09, 0xd9, 0x3b, 0x44, 0x0c, 0x21, 0x2d, 0x93, 0xf0, 0x70, 0x28, 0xc1, 0x4b, 0xfc, 0xf9, 0xa2, + 0x98, 0x22, 0x61, 0xe5, 0xc0, 0x28, 0xc8, 0xb3, 0xe3, 0x82, 0xe5, 0xa7, 0x6c, 0xac, 0x39, 0xa0, 0x00, 0xac, 0xa8, + 0x39, 0x18, 0x2f, 0x34, 0xa3, 0xa3, 0x74, 0x28, 0x83, 0xa1, 0x7a, 0xa6, 0x98, 0x65, 0x92, 0x99, 0xb5, 0x85, 0xa3, + 0xa5, 0x80, 0x23, 0x8c, 0x0a, 0x29, 0x09, 0xf2, 0x50, 0x61, 0x38, 0x05, 0x29, 0x04, 0x5a, 0xc1, 0xdc, 0xe6, 0x4a, + 0x93, 0x3d, 0x5b, 0x90, 0x4a, 0xc8, 0xa1, 0x23, 0x6c, 0x64, 0x82, 0x34, 0x77, 0x61, 0x57, 0x81, 0x94, 0x97, 0xe0, + 0x0a, 0x29, 0xa2, 0xcc, 0x1c, 0x64, 0x80, 0xf0, 0x5b, 0xa1, 0x0b, 0x7d, 0x6c, 0x41, 0x6c, 0xe0, 0xeb, 0x95, 0x07, + 0xc2, 0x4a, 0xbc, 0x2b, 0x44, 0xbc, 0x2b, 0xc0, 0xc6, 0x89, 0x91, 0x9f, 0xbc, 0x3b, 0xdc, 0x4f, 0xb3, 0xbd, 0xd1, + 0x88, 0x15, 0x45, 0x06, 0xb0, 0xdd, 0xa1, 0xf6, 0x57, 0x19, 0x5a, 0x40, 0x49, 0x57, 0xcb, 0x3a, 0xbb, 0x20, 0x0d, + 0x6e, 0xaa, 0x15, 0xa5, 0xd3, 0x03, 0xfb, 0xe3, 0x47, 0x90, 0xd9, 0x9e, 0x24, 0x03, 0x50, 0x7d, 0xd5, 0xf0, 0x13, + 0xf6, 0x4c, 0x9d, 0x32, 0x6b, 0xed, 0x4b, 0xa7, 0x0e, 0x92, 0x07, 0xc3, 0xba, 0xa5, 0xb1, 0xa0, 0x6b, 0x87, 0xc6, + 0xd5, 0x90, 0x0a, 0x72, 0x79, 0x42, 0x2a, 0xdb, 0x58, 0x46, 0xb0, 0xda, 0x4a, 0x8f, 0x48, 0xaf, 0xb0, 0x29, 0x08, + 0xd0, 0x43, 0x36, 0xec, 0xc9, 0xfa, 0x30, 0x17, 0x94, 0xcb, 0xd9, 0xaf, 0x0b, 0x56, 0x70, 0xc1, 0xba, 0x30, 0x6e, + 0x01, 0xe3, 0x96, 0x2b, 0xd6, 0x61, 0xcd, 0x76, 0x5c, 0x07, 0xdb, 0x9b, 0x39, 0xea, 0xb1, 0x02, 0x39, 0xf9, 0x7a, + 0x76, 0x42, 0x58, 0x99, 0x7b, 0x79, 0xf9, 0xad, 0x1a, 0xa4, 0x5a, 0x4a, 0x6d, 0x03, 0x35, 0xd6, 0xc4, 0x56, 0x4d, + 0xc6, 0xb6, 0x2b, 0x15, 0xea, 0x9d, 0x4e, 0xaf, 0xc6, 0x07, 0xb0, 0xe7, 0xda, 0x9a, 0xa5, 0x2b, 0x63, 0xfb, 0xad, + 0xa2, 0xe9, 0x1b, 0x31, 0x32, 0x59, 0xa3, 0xec, 0x66, 0xee, 0x51, 0x3b, 0x1e, 0xda, 0xae, 0xd4, 0x55, 0x82, 0x61, + 0x51, 0x17, 0x0c, 0x4d, 0xa8, 0xe7, 0xba, 0x8b, 0xad, 0x99, 0x8a, 0x85, 0x6a, 0xad, 0x95, 0x03, 0xc1, 0xc3, 0x43, + 0x30, 0x4e, 0xd6, 0xfa, 0x07, 0xaf, 0xa3, 0x19, 0x43, 0x8a, 0x7a, 0x57, 0x35, 0x90, 0x0e, 0x04, 0x34, 0x19, 0x36, + 0xd5, 0x1b, 0x77, 0x85, 0xd5, 0x54, 0xdf, 0x5f, 0x31, 0x58, 0x11, 0x60, 0x5f, 0x97, 0x6b, 0x96, 0x88, 0xf4, 0xa6, + 0xe0, 0x12, 0x4d, 0x1f, 0x51, 0x26, 0xd6, 0x84, 0x14, 0x3c, 0x20, 0x0f, 0xcb, 0xdf, 0x58, 0x38, 0xd9, 0x8a, 0x29, + 0x1c, 0x39, 0xca, 0x14, 0xa0, 0x33, 0x29, 0x01, 0x10, 0x97, 0xf4, 0xb3, 0xb6, 0xb1, 0x90, 0x6c, 0xfb, 0xc8, 0x07, + 0xfe, 0x24, 0x89, 0xb8, 0xd3, 0xd9, 0x6a, 0xbb, 0xc0, 0x87, 0x20, 0xc4, 0x41, 0x47, 0x80, 0x79, 0x5f, 0xa1, 0xc2, + 0x10, 0x95, 0xd8, 0xe5, 0x3e, 0x18, 0x45, 0xd3, 0x78, 0xc2, 0x9d, 0x0c, 0x95, 0x88, 0x5b, 0xb2, 0x04, 0x94, 0x8c, + 0xde, 0x57, 0x20, 0x25, 0xb8, 0x90, 0x2e, 0xa2, 0x5a, 0x0b, 0x34, 0x05, 0x29, 0x49, 0x29, 0xd2, 0x82, 0x0a, 0x02, + 0x43, 0xa8, 0xf4, 0x14, 0x47, 0x81, 0x7e, 0x8b, 0x07, 0x62, 0xd0, 0x60, 0xc5, 0xa2, 0x8c, 0x07, 0xf1, 0x6a, 0x21, + 0xa8, 0x61, 0x9f, 0x67, 0x2f, 0xb3, 0x33, 0x96, 0x3f, 0x89, 0x10, 0xf6, 0x40, 0x74, 0x2f, 0x41, 0xd2, 0x93, 0x40, + 0x67, 0x3d, 0xc5, 0x2b, 0xa7, 0x84, 0x34, 0x2c, 0xc4, 0x2c, 0x46, 0x45, 0x08, 0x5a, 0x8e, 0x68, 0x9f, 0xe2, 0x96, + 0xa2, 0xbd, 0x87, 0xaa, 0x84, 0x69, 0xde, 0xda, 0x7b, 0x59, 0xe7, 0x2d, 0x18, 0x61, 0xae, 0xb8, 0xb5, 0xbe, 0x63, + 0x5d, 0x4f, 0xea, 0x66, 0x47, 0xf2, 0x96, 0xa1, 0xcc, 0x40, 0x7f, 0x5c, 0x5e, 0x56, 0x46, 0x3a, 0x28, 0x53, 0x2d, + 0xcd, 0xd1, 0x72, 0x12, 0x5b, 0xc2, 0x2d, 0x41, 0x19, 0xa1, 0xe1, 0x95, 0x67, 0x49, 0x62, 0xe8, 0x22, 0x2f, 0xee, + 0x39, 0x0d, 0x75, 0x04, 0x50, 0xcc, 0x6a, 0x1a, 0x69, 0xc0, 0x03, 0x5d, 0x81, 0x4a, 0x49, 0x69, 0x23, 0xaf, 0x6a, + 0x22, 0x20, 0x4e, 0xc7, 0x2c, 0x17, 0x0e, 0x9a, 0xd4, 0xa1, 0x30, 0x61, 0x0a, 0x0c, 0xcd, 0xc6, 0x20, 0xe1, 0x15, + 0x02, 0x60, 0x9e, 0xf8, 0xd3, 0xac, 0xe0, 0xba, 0xce, 0x84, 0x3e, 0xbe, 0xbc, 0x8c, 0x85, 0xbf, 0x88, 0x0c, 0x90, + 0xb3, 0x59, 0x76, 0xca, 0xd6, 0x40, 0xdd, 0x53, 0x83, 0x99, 0x20, 0x1b, 0xc3, 0x80, 0x12, 0x05, 0xd5, 0x32, 0x4f, + 0xe2, 0x11, 0xd3, 0x5a, 0x6a, 0xe6, 0x83, 0x41, 0xc7, 0xce, 0x41, 0x46, 0x30, 0xb7, 0xdf, 0xef, 0xb7, 0xbd, 0x8e, + 0x5b, 0x0a, 0x82, 0x2f, 0x57, 0x28, 0x7a, 0x8d, 0x7e, 0x94, 0x26, 0xf8, 0x3a, 0x59, 0xc0, 0x5d, 0x43, 0x29, 0x72, + 0xe1, 0x27, 0x79, 0x52, 0x10, 0xbb, 0xde, 0x18, 0x06, 0xe5, 0x4c, 0x09, 0x6e, 0x34, 0x71, 0xc5, 0xb6, 0x7d, 0xa7, + 0xc9, 0xa6, 0xd9, 0x49, 0xed, 0x30, 0xb5, 0x30, 0x72, 0xcd, 0x0b, 0xed, 0x01, 0x9b, 0xcb, 0x83, 0x56, 0x22, 0x55, + 0x03, 0xaf, 0x03, 0x84, 0xc2, 0xd3, 0x75, 0x56, 0x50, 0xaa, 0x3a, 0x4b, 0x21, 0xae, 0x37, 0xd0, 0x5b, 0x26, 0xc1, + 0x5c, 0x47, 0x82, 0x7d, 0x29, 0x10, 0x38, 0x7a, 0x64, 0x62, 0xbd, 0x9e, 0xc0, 0xf2, 0x1c, 0x47, 0xa3, 0x4f, 0x1a, + 0xdc, 0x8a, 0xec, 0x4d, 0x36, 0x70, 0x1a, 0x25, 0xa1, 0x21, 0xae, 0x4c, 0xbc, 0x95, 0x84, 0xae, 0x6d, 0x14, 0x70, + 0xc8, 0x56, 0xd8, 0xbe, 0xb9, 0xd0, 0x4d, 0x6e, 0x97, 0xec, 0xa1, 0xfc, 0x27, 0xcd, 0x25, 0xd7, 0xb0, 0x1c, 0x57, + 0xd2, 0x80, 0x2b, 0xc6, 0x83, 0xa5, 0x69, 0x40, 0x02, 0x7c, 0x57, 0x8e, 0xe3, 0xe2, 0x6a, 0x12, 0xfc, 0xa1, 0x60, + 0x3e, 0x35, 0x66, 0xba, 0x11, 0x52, 0x2d, 0xe1, 0xa4, 0x19, 0xac, 0x41, 0x93, 0xc6, 0x83, 0x12, 0x35, 0xdf, 0xa2, + 0xa1, 0x42, 0x1c, 0x7f, 0x22, 0xaa, 0xd0, 0x04, 0x43, 0x30, 0x72, 0xaf, 0x90, 0x0c, 0x97, 0xad, 0x8a, 0x16, 0x29, + 0x53, 0x63, 0x52, 0xa9, 0x9a, 0xe5, 0x32, 0x30, 0xb0, 0x68, 0xb7, 0xfa, 0xd2, 0x12, 0x57, 0x22, 0x37, 0x0d, 0xb5, + 0x30, 0x29, 0x94, 0x37, 0xe1, 0xe4, 0xe8, 0x77, 0x29, 0xeb, 0xdd, 0xc4, 0x27, 0x57, 0xf8, 0xe4, 0xbe, 0xe1, 0x43, + 0x99, 0xbc, 0x5d, 0x0c, 0x8a, 0xe0, 0x9b, 0x5a, 0x25, 0xda, 0xa7, 0x3e, 0x0a, 0x66, 0x57, 0x0b, 0x5d, 0x10, 0x28, + 0x92, 0x4d, 0xd2, 0x81, 0xe4, 0x37, 0x14, 0x1b, 0x95, 0x67, 0x94, 0xb9, 0x62, 0x83, 0xd4, 0xbc, 0xd2, 0xcc, 0x4b, + 0xdd, 0x86, 0xfd, 0x5e, 0x96, 0x92, 0x4e, 0x5c, 0x50, 0x26, 0xf6, 0xae, 0xa3, 0x8d, 0x97, 0x86, 0x99, 0xb0, 0x7e, + 0x85, 0xb1, 0x53, 0xa3, 0x50, 0x2a, 0x45, 0x20, 0x8e, 0x8d, 0xaf, 0x95, 0x65, 0x90, 0xf9, 0x6b, 0xec, 0x29, 0x00, + 0x25, 0x81, 0xc5, 0xd7, 0x54, 0xf2, 0xa2, 0xb0, 0x4e, 0xc7, 0x3b, 0x44, 0xc7, 0x4a, 0x84, 0xd6, 0x44, 0xbe, 0xd6, + 0x67, 0xb1, 0x5f, 0x73, 0x09, 0x4d, 0x4a, 0xe6, 0x83, 0x3c, 0xb0, 0x55, 0x20, 0xa2, 0xd2, 0x6d, 0xc9, 0x20, 0x21, + 0x87, 0x74, 0x95, 0xe8, 0xb5, 0x91, 0x0c, 0x5a, 0xa7, 0x42, 0xa2, 0xa5, 0xc3, 0x30, 0x72, 0xd0, 0x71, 0xa7, 0xb5, + 0x58, 0x21, 0x64, 0xd3, 0xde, 0x24, 0x56, 0x44, 0xe7, 0x34, 0x47, 0x13, 0xce, 0xd4, 0xe9, 0x8e, 0x03, 0xe8, 0x80, + 0xd8, 0x5f, 0x61, 0xbd, 0xb5, 0x66, 0xa7, 0xeb, 0x57, 0x0e, 0xdf, 0xe5, 0x65, 0x82, 0xfc, 0x20, 0x0c, 0x5e, 0x58, + 0xb3, 0x81, 0x92, 0xbd, 0x7b, 0x2f, 0xb1, 0x15, 0xd9, 0x9f, 0x55, 0x49, 0xe5, 0x29, 0xd4, 0x38, 0xb7, 0xbe, 0x4e, + 0xcc, 0x0c, 0x2d, 0xaa, 0x8a, 0x7d, 0x43, 0xaa, 0xef, 0x2b, 0x85, 0x5d, 0xa1, 0xbc, 0x2f, 0x87, 0x8e, 0x5d, 0xd7, + 0x0d, 0x72, 0x72, 0x5e, 0xee, 0xac, 0x73, 0x21, 0xef, 0xde, 0x35, 0x7d, 0xa6, 0x53, 0x3d, 0xfc, 0x13, 0x07, 0x95, + 0x73, 0x71, 0x91, 0x92, 0x05, 0xf3, 0x44, 0xa9, 0xa3, 0x15, 0x07, 0xb4, 0xdd, 0x43, 0x4f, 0x3b, 0x3a, 0x8b, 0x62, + 0x6e, 0xe9, 0x51, 0x84, 0xa7, 0x8d, 0xf2, 0x49, 0x1a, 0x1d, 0x80, 0x17, 0x9a, 0x90, 0xe4, 0x84, 0x9b, 0xb6, 0x68, + 0x31, 0x9a, 0x32, 0x0c, 0x81, 0x2b, 0x7b, 0xc2, 0x94, 0x3d, 0x77, 0x10, 0x6f, 0x31, 0x30, 0x5b, 0x0f, 0x7b, 0xd9, + 0xec, 0x5e, 0x33, 0xff, 0x61, 0x8d, 0x40, 0xb6, 0xcd, 0x54, 0x5d, 0xd9, 0x78, 0x97, 0x22, 0x12, 0x23, 0x6c, 0xeb, + 0xc6, 0x96, 0xb6, 0x7e, 0xaf, 0xe1, 0x5e, 0x57, 0x8e, 0x79, 0x4d, 0xa9, 0x36, 0xf4, 0xb0, 0x72, 0x73, 0x98, 0xe9, + 0xc8, 0x8b, 0x15, 0x74, 0x7b, 0x22, 0x28, 0x04, 0x4e, 0x84, 0xb6, 0x07, 0x15, 0x37, 0x10, 0x29, 0xb9, 0xd2, 0xaa, + 0xd9, 0x22, 0x19, 0x4b, 0x60, 0xc1, 0x85, 0xe5, 0x92, 0x8f, 0xce, 0xe2, 0x24, 0xa9, 0x4a, 0xff, 0x50, 0x01, 0x2f, + 0x86, 0xbd, 0x49, 0xb4, 0x0b, 0x8c, 0x16, 0x0a, 0x04, 0x57, 0x1b, 0x61, 0xef, 0x1d, 0xb7, 0x5a, 0x77, 0x11, 0x71, + 0xe4, 0x66, 0x34, 0x02, 0xea, 0x31, 0xc2, 0xaa, 0x59, 0x7b, 0xef, 0x19, 0x86, 0xd4, 0x0c, 0x7c, 0x50, 0x9d, 0x51, + 0xf1, 0x67, 0xd9, 0x53, 0x9f, 0x89, 0xde, 0x8d, 0xaa, 0xab, 0x19, 0x50, 0x51, 0x81, 0x0f, 0x33, 0xc4, 0xd2, 0x56, + 0x81, 0x80, 0x5c, 0x0f, 0x8b, 0x52, 0xc0, 0x24, 0x0d, 0x16, 0x94, 0x02, 0x6b, 0xad, 0xec, 0x5e, 0xde, 0x14, 0xcc, + 0xa1, 0x50, 0xb8, 0xe8, 0xff, 0x24, 0x9b, 0xcd, 0xd1, 0x32, 0x6b, 0x30, 0x35, 0x34, 0x78, 0xdf, 0xa8, 0x2f, 0xd7, + 0x94, 0xd5, 0xfa, 0xd0, 0x8e, 0xac, 0xf1, 0x93, 0x76, 0x94, 0xc1, 0xa1, 0x5a, 0xe8, 0xa2, 0xba, 0xdd, 0xdc, 0x14, + 0x31, 0xeb, 0x78, 0xdc, 0x27, 0xbd, 0xad, 0xad, 0x49, 0x4f, 0xd3, 0x80, 0x64, 0x92, 0x64, 0x78, 0x93, 0x01, 0xca, + 0x8a, 0x38, 0xcb, 0xb2, 0x41, 0xbe, 0x65, 0x59, 0xe2, 0xfa, 0x7d, 0xd7, 0xdb, 0xab, 0x79, 0xd6, 0xde, 0xde, 0xd5, + 0x2e, 0x72, 0x55, 0x27, 0x3d, 0xc8, 0xc3, 0x21, 0x14, 0xad, 0xd8, 0x94, 0xe1, 0x72, 0x96, 0x8d, 0x59, 0x60, 0x43, + 0xf7, 0xd4, 0x2e, 0x95, 0x56, 0x86, 0xcd, 0x91, 0x32, 0x67, 0xf9, 0xae, 0x1e, 0x49, 0x0d, 0xf6, 0x80, 0x05, 0xb4, + 0xb9, 0xf0, 0x7d, 0x78, 0x92, 0x64, 0xc7, 0x51, 0x72, 0x20, 0x14, 0x78, 0xad, 0xe5, 0x07, 0x70, 0x19, 0xc9, 0x62, + 0x35, 0x94, 0xd4, 0xf7, 0x83, 0xef, 0x83, 0x9b, 0x7b, 0x54, 0xde, 0x8a, 0xdd, 0xf1, 0xdb, 0x7e, 0xc7, 0x56, 0x11, + 0xb1, 0x97, 0xe6, 0x74, 0xa0, 0x71, 0x0a, 0xa0, 0xcc, 0x01, 0x68, 0xb2, 0xc2, 0x8b, 0x58, 0xf8, 0x72, 0xf0, 0x52, + 0xb9, 0xd4, 0x19, 0xb8, 0x10, 0xe0, 0xe4, 0x27, 0x31, 0x6f, 0xe1, 0x79, 0xa4, 0xed, 0x2d, 0x45, 0x05, 0xc6, 0x15, + 0x29, 0x2e, 0x5d, 0x2a, 0x6f, 0xd0, 0xfb, 0x18, 0x1e, 0x41, 0xb3, 0x8d, 0x8d, 0xa5, 0xf3, 0x2a, 0xe2, 0x53, 0x3f, + 0x8f, 0xd2, 0x71, 0x36, 0x73, 0xdc, 0x4d, 0xdb, 0x76, 0xfd, 0x82, 0x3c, 0x91, 0xaf, 0xdc, 0x72, 0xe3, 0xc8, 0x9b, + 0xb2, 0xd0, 0x1e, 0xd8, 0x9b, 0x1f, 0xbd, 0xf7, 0x2c, 0x3c, 0xda, 0xdd, 0x58, 0x4e, 0x59, 0xd9, 0x3f, 0xf2, 0xce, + 0x75, 0xcc, 0xdd, 0x7b, 0x8b, 0x52, 0x06, 0x7a, 0x85, 0xfd, 0x73, 0x09, 0x06, 0xb0, 0x1b, 0xc5, 0xdf, 0x41, 0xca, + 0xbd, 0xa7, 0x03, 0x11, 0x19, 0xa7, 0xbd, 0xbc, 0xb4, 0x33, 0x8a, 0x18, 0xd8, 0x77, 0xb4, 0xb3, 0x7a, 0xf7, 0x6e, + 0xa5, 0xe6, 0xab, 0x52, 0x6f, 0xc4, 0xc2, 0x9a, 0xa7, 0xee, 0x1d, 0xd0, 0xd1, 0x4a, 0x7d, 0x23, 0x8f, 0x18, 0x29, + 0xcd, 0x55, 0x3b, 0xc1, 0x31, 0xb6, 0xf8, 0xfa, 0x6d, 0x7d, 0x28, 0xa2, 0x14, 0x7e, 0x0c, 0xd6, 0x4b, 0x04, 0xea, + 0x1b, 0x1c, 0x1c, 0xef, 0x20, 0xdc, 0xda, 0x75, 0x06, 0x81, 0x73, 0xa7, 0xd5, 0xba, 0xfc, 0x69, 0xeb, 0xf0, 0xe7, + 0xa8, 0xf5, 0xdb, 0x5e, 0xeb, 0xc7, 0xa1, 0x7b, 0xe9, 0xfc, 0xb4, 0x35, 0x38, 0x94, 0x6f, 0x87, 0x3f, 0xf7, 0x7f, + 0x2a, 0x86, 0x5f, 0x8a, 0xc2, 0x0d, 0xd7, 0xdd, 0x3a, 0x01, 0x4f, 0x29, 0xdc, 0x6a, 0xb5, 0xfa, 0xf0, 0xb4, 0x80, + 0x27, 0xfc, 0x79, 0x06, 0x3f, 0x2e, 0x0f, 0xad, 0xff, 0xf0, 0x53, 0xfa, 0x1f, 0x7f, 0xca, 0x87, 0x38, 0xe6, 0xe1, + 0xcf, 0x3f, 0x15, 0xf6, 0xbd, 0x7e, 0xb8, 0x35, 0xdc, 0x74, 0x1d, 0x5d, 0xf3, 0x65, 0x58, 0x3d, 0x42, 0xab, 0xc3, + 0x9f, 0xe5, 0x9b, 0x7d, 0xef, 0x68, 0xb7, 0x1f, 0x0e, 0x2f, 0x1d, 0xfb, 0xf2, 0x9e, 0x7b, 0xe9, 0xba, 0x97, 0x1b, + 0x38, 0xcf, 0x1c, 0x46, 0xbf, 0x07, 0x3f, 0x4f, 0xe0, 0xa7, 0x0d, 0x3f, 0x4f, 0xe1, 0xe7, 0xcf, 0xd0, 0x4d, 0xc4, + 0xdf, 0x2e, 0x29, 0x16, 0x72, 0x89, 0x07, 0x16, 0x11, 0xac, 0x82, 0xbb, 0xb1, 0x15, 0x7b, 0x13, 0x22, 0x1a, 0xec, + 0x43, 0xdf, 0xf7, 0x31, 0x4c, 0xea, 0x2c, 0x3f, 0x6e, 0xc0, 0xa2, 0x23, 0xe7, 0x6c, 0x04, 0xcc, 0x13, 0x91, 0x83, + 0x22, 0xe0, 0xe2, 0x6c, 0xb5, 0xc0, 0xc3, 0x55, 0x6f, 0x1c, 0x4e, 0x98, 0x03, 0x46, 0xc1, 0x73, 0x86, 0x0f, 0x5d, + 0xd7, 0x7b, 0x26, 0xcf, 0x0c, 0x71, 0x9f, 0x0b, 0xd6, 0x4a, 0x33, 0x61, 0xd2, 0xd8, 0xae, 0x37, 0x5f, 0x53, 0x09, + 0xdb, 0x3a, 0x3d, 0x81, 0xba, 0x0d, 0x71, 0xd0, 0xf6, 0x3d, 0x8b, 0x3e, 0xe1, 0x96, 0x7c, 0x6d, 0x1c, 0x02, 0xaf, + 0x58, 0xf2, 0x4d, 0xa3, 0xd1, 0xb0, 0x11, 0x85, 0x3b, 0xf6, 0x98, 0xc1, 0x0c, 0x2b, 0x26, 0x22, 0x27, 0xa5, 0x29, + 0x2c, 0x5b, 0x98, 0xfc, 0x6d, 0x94, 0xf3, 0x8d, 0xca, 0xb0, 0x0d, 0x6b, 0x96, 0x6c, 0xd3, 0xd2, 0xbf, 0xc5, 0x14, + 0x68, 0x5a, 0xd2, 0xf9, 0x87, 0x39, 0x7e, 0x98, 0x12, 0x5a, 0xaf, 0x1d, 0x0e, 0x1e, 0x7a, 0x01, 0x72, 0x47, 0xf4, + 0x73, 0xde, 0xa2, 0x1a, 0x83, 0xbf, 0x32, 0xcc, 0xe0, 0x89, 0xf9, 0x30, 0x44, 0xb3, 0x2c, 0x75, 0x70, 0x2b, 0x45, + 0x71, 0xff, 0x02, 0x77, 0x46, 0x5a, 0x7a, 0xfb, 0xa1, 0xda, 0x31, 0x07, 0x39, 0x63, 0xdf, 0x47, 0xc9, 0x27, 0x96, + 0x3b, 0xe7, 0x5e, 0xa7, 0xfb, 0x15, 0x75, 0xf6, 0xd0, 0x36, 0x7b, 0x55, 0x1d, 0xa3, 0x29, 0xb3, 0x40, 0x1d, 0x11, + 0xb6, 0x3a, 0x5e, 0x8e, 0x51, 0x2d, 0x24, 0x41, 0xe1, 0x65, 0x61, 0x97, 0x38, 0xdc, 0xde, 0x2d, 0x4e, 0x4f, 0xfa, + 0x76, 0x60, 0xdb, 0x60, 0xf1, 0x1f, 0x50, 0xd8, 0x4a, 0x18, 0x16, 0x60, 0x90, 0xed, 0xc6, 0x3d, 0xbe, 0xb9, 0x59, + 0x05, 0x9c, 0xf0, 0x20, 0x9d, 0xba, 0x27, 0x5e, 0xe4, 0x4d, 0x43, 0x18, 0x70, 0x04, 0xcd, 0xb0, 0x4b, 0x6f, 0xb4, + 0x1b, 0xcb, 0x69, 0x30, 0x16, 0xe2, 0x27, 0x51, 0xc1, 0x5f, 0x60, 0x3c, 0x22, 0x1c, 0xa1, 0xb1, 0xef, 0xb3, 0x73, + 0x36, 0x52, 0x76, 0x06, 0x10, 0x2a, 0x72, 0x7b, 0xee, 0x28, 0x34, 0x9a, 0xc1, 0xdc, 0x61, 0x78, 0x30, 0xb0, 0x61, + 0x2f, 0xc1, 0xae, 0x0c, 0xa3, 0xc3, 0xce, 0x70, 0x90, 0x86, 0x20, 0x6b, 0x35, 0x6d, 0x65, 0xd1, 0xa2, 0x56, 0xd4, + 0x1d, 0x0e, 0x9c, 0x53, 0x30, 0xd2, 0xc1, 0x16, 0x77, 0xf0, 0x0d, 0x23, 0x14, 0x45, 0xf8, 0x8e, 0x9d, 0x3c, 0x3b, + 0x9f, 0x3b, 0xf6, 0xee, 0x96, 0xbd, 0x89, 0xa5, 0x9e, 0x0d, 0xec, 0x05, 0x73, 0x87, 0x67, 0xae, 0xd9, 0x79, 0x7b, + 0x88, 0xa0, 0x62, 0x21, 0x4e, 0x7e, 0x36, 0xb0, 0xfb, 0x62, 0xea, 0x36, 0x0c, 0x9a, 0xca, 0xe5, 0xc7, 0x15, 0x3d, + 0x20, 0x54, 0x55, 0x57, 0x05, 0x1d, 0x94, 0x75, 0x03, 0x67, 0x6a, 0x22, 0xd1, 0xc2, 0xc9, 0x24, 0x15, 0xc0, 0xe1, + 0xc1, 0x66, 0x30, 0xa9, 0xd1, 0x6d, 0x7b, 0x38, 0x38, 0x0b, 0xee, 0xd9, 0xf7, 0xd4, 0xcb, 0x09, 0x0b, 0xc0, 0xbb, + 0xa0, 0xe9, 0x4f, 0x50, 0x8b, 0xc0, 0xcf, 0x39, 0x03, 0x24, 0xcf, 0xa8, 0x68, 0x2c, 0x8b, 0x16, 0x58, 0x74, 0x10, + 0x20, 0xa8, 0x5e, 0xa1, 0xad, 0x3f, 0xb1, 0x26, 0xe3, 0x90, 0x60, 0xbf, 0x7b, 0x17, 0x96, 0x66, 0xb3, 0x33, 0xc4, + 0xf3, 0x86, 0x9c, 0x17, 0xdf, 0xc7, 0x1c, 0x54, 0xc2, 0x56, 0xdf, 0x76, 0x07, 0xb6, 0x85, 0x4b, 0xdb, 0xcb, 0x36, + 0x43, 0x41, 0xe1, 0x78, 0xf3, 0x3d, 0x0b, 0xa6, 0xfd, 0xb0, 0x3d, 0x70, 0x72, 0xa1, 0x3a, 0x12, 0x3c, 0xb7, 0x14, + 0x12, 0xbc, 0xed, 0x4d, 0x41, 0xa0, 0x23, 0xe7, 0x6e, 0xd8, 0x9b, 0xaa, 0x10, 0x8a, 0x3e, 0x6e, 0x8e, 0xdd, 0x20, + 0x86, 0x1f, 0x4e, 0x0b, 0x99, 0x66, 0xaa, 0xfb, 0x6a, 0xcd, 0xec, 0x06, 0x63, 0x65, 0x91, 0x27, 0x61, 0xb6, 0xe9, + 0x60, 0x84, 0x16, 0x24, 0xed, 0xee, 0x00, 0x60, 0xd8, 0x74, 0x14, 0xa7, 0x6d, 0x29, 0x56, 0x53, 0xf6, 0xf9, 0x61, + 0xb5, 0x1c, 0x6c, 0x10, 0x31, 0xbf, 0xd2, 0x3e, 0x00, 0x56, 0x90, 0x78, 0xf9, 0x50, 0x9d, 0x79, 0x3d, 0xaf, 0x9d, + 0x6f, 0x2d, 0x95, 0x28, 0x62, 0x9e, 0x21, 0xa1, 0x78, 0xa9, 0xdd, 0x30, 0x61, 0x6e, 0xcf, 0x91, 0x18, 0x9a, 0xe5, + 0xc3, 0x36, 0x30, 0xbd, 0x0a, 0xb0, 0xa7, 0xe6, 0xb6, 0x48, 0xc2, 0xaa, 0xb9, 0x77, 0x08, 0xac, 0x3d, 0x0c, 0x5f, + 0x89, 0x13, 0xc7, 0x9e, 0x8a, 0xe6, 0xb3, 0x24, 0x7c, 0xde, 0x38, 0x2e, 0x8e, 0xf0, 0x44, 0x68, 0xdf, 0x1f, 0x2d, + 0x72, 0x90, 0x07, 0xfc, 0x35, 0x58, 0x06, 0xa1, 0x6c, 0x8a, 0x8e, 0x1e, 0x1e, 0x01, 0x7b, 0x84, 0x78, 0x23, 0x6c, + 0x6e, 0x54, 0xa3, 0x45, 0x49, 0xc6, 0x0b, 0x1d, 0x0c, 0xf7, 0xb8, 0x74, 0xed, 0x51, 0x30, 0xc8, 0x13, 0x63, 0x07, + 0xcf, 0xfc, 0xfd, 0x11, 0x56, 0xe3, 0x04, 0x85, 0x5b, 0xd2, 0x6e, 0xab, 0xc4, 0xdf, 0xbe, 0x9f, 0x82, 0x04, 0xc7, + 0x3a, 0xf0, 0xb3, 0xee, 0xde, 0x4d, 0x24, 0x52, 0xbb, 0x69, 0x8f, 0x4e, 0x22, 0x30, 0x1e, 0x9c, 0xfb, 0x29, 0x54, + 0x23, 0x89, 0xa8, 0x28, 0x47, 0x0b, 0xd4, 0x3c, 0x55, 0xab, 0xe0, 0x3b, 0x34, 0x23, 0xf0, 0x1c, 0xc3, 0xd6, 0xe4, + 0xa7, 0xea, 0xc6, 0x22, 0x96, 0xef, 0xba, 0x74, 0xb4, 0x85, 0x07, 0x90, 0x82, 0xd1, 0x04, 0xc3, 0xb8, 0x14, 0x94, + 0xac, 0xf8, 0xef, 0xa3, 0x11, 0x2b, 0x9f, 0x1e, 0x66, 0x9b, 0x9b, 0x43, 0x71, 0x6e, 0x41, 0x8c, 0xc3, 0x8d, 0xe8, + 0x6a, 0x5c, 0x01, 0x50, 0x9f, 0xce, 0x89, 0xeb, 0x81, 0x69, 0xc5, 0x9a, 0x2e, 0xc5, 0x3e, 0x39, 0xcc, 0x00, 0x14, + 0xdc, 0x72, 0x0e, 0xfd, 0xc1, 0x9f, 0x86, 0xe0, 0x1e, 0xfb, 0x5f, 0xba, 0x5b, 0x4a, 0xd0, 0xf4, 0xe4, 0x99, 0xe2, + 0x92, 0xce, 0x58, 0x3b, 0x1e, 0xc5, 0x46, 0x83, 0xc2, 0x4b, 0x01, 0x03, 0xd0, 0xe6, 0x20, 0x13, 0x2a, 0x0e, 0x42, + 0x8e, 0x0a, 0x6c, 0x1f, 0x37, 0x3f, 0xc7, 0x9d, 0xfd, 0x14, 0x2c, 0xbc, 0x81, 0x7e, 0x7b, 0x0c, 0x6f, 0x7f, 0xd2, + 0x6f, 0x2f, 0x59, 0xf0, 0x4b, 0x29, 0x43, 0xf7, 0xb5, 0x29, 0x1e, 0xa8, 0x29, 0x4a, 0xb1, 0x44, 0x06, 0x0d, 0x99, + 0x9b, 0xaf, 0xc4, 0x6c, 0xb8, 0x5b, 0xa2, 0xda, 0x91, 0xa2, 0x2b, 0xf7, 0x79, 0x74, 0x82, 0xc4, 0x75, 0x4d, 0x52, + 0x18, 0xb9, 0x04, 0x26, 0xc2, 0x15, 0xdf, 0x12, 0x73, 0xf6, 0xdb, 0x60, 0x83, 0xd7, 0xf2, 0x0e, 0xd0, 0xbe, 0x63, + 0xb3, 0x39, 0xbf, 0xd8, 0x27, 0x45, 0x1f, 0xc8, 0xb4, 0x01, 0x71, 0x76, 0xde, 0xee, 0xc5, 0xbb, 0xbc, 0x17, 0x83, + 0x54, 0xcf, 0x15, 0x8b, 0xe1, 0x5e, 0xf5, 0xde, 0x62, 0x94, 0xd2, 0x64, 0x26, 0xaf, 0x86, 0x5e, 0x57, 0xa2, 0xb7, + 0xb9, 0x09, 0x08, 0xf6, 0x8c, 0xae, 0x5c, 0x74, 0x2d, 0x4b, 0x41, 0x13, 0x80, 0xe8, 0x51, 0x9d, 0xe5, 0x88, 0xe3, + 0x30, 0x9b, 0x0d, 0x05, 0x07, 0x73, 0xd7, 0x8e, 0x8a, 0x63, 0x62, 0x77, 0x99, 0xb0, 0x03, 0x98, 0x11, 0x97, 0xb7, + 0x3a, 0x22, 0x3a, 0x2c, 0xfa, 0xeb, 0xf8, 0xf6, 0x47, 0x8f, 0x6d, 0x76, 0x5c, 0xd0, 0x20, 0xb5, 0xb1, 0x1e, 0x56, + 0x63, 0x41, 0x7d, 0xf8, 0x51, 0x53, 0xa9, 0x2c, 0x36, 0x37, 0xcb, 0xfa, 0x51, 0xad, 0xda, 0xc1, 0xb5, 0xd3, 0x94, + 0xf3, 0x66, 0x36, 0x08, 0x07, 0x22, 0x26, 0x50, 0xa0, 0xa5, 0x95, 0x15, 0x03, 0x0c, 0x29, 0xcb, 0x51, 0x3e, 0x85, + 0xcc, 0x8b, 0xcb, 0x52, 0xa7, 0xbe, 0xc8, 0x78, 0x64, 0x88, 0xa7, 0x9e, 0x64, 0xac, 0x80, 0x82, 0xf5, 0x52, 0x2f, + 0xa1, 0x25, 0x02, 0xcc, 0x9f, 0xa9, 0x1c, 0x1a, 0x61, 0x81, 0x44, 0xa1, 0x61, 0x96, 0x28, 0xe3, 0xb3, 0x08, 0x63, + 0xd0, 0xf6, 0x4f, 0x6a, 0xb1, 0xaf, 0x42, 0x19, 0x1d, 0xc5, 0x61, 0x3e, 0x0c, 0xa8, 0x7e, 0x21, 0x25, 0xd8, 0x34, + 0x7c, 0x0f, 0x6c, 0x54, 0x39, 0x9e, 0x24, 0x08, 0x9f, 0xc6, 0x39, 0x23, 0x4f, 0x61, 0x43, 0xc2, 0x2c, 0x4d, 0xdb, + 0x48, 0xb5, 0x8b, 0xcc, 0x20, 0x94, 0x0b, 0xf3, 0x4f, 0x8d, 0xb3, 0x8b, 0x2c, 0x5c, 0x69, 0x0d, 0xe6, 0xc7, 0x1b, + 0x13, 0xa0, 0xec, 0xf2, 0x32, 0x13, 0x3e, 0x6e, 0x44, 0xf6, 0x86, 0xae, 0x98, 0x0e, 0x14, 0x52, 0x81, 0x13, 0x91, + 0xc5, 0x43, 0x67, 0x28, 0x34, 0xc2, 0x01, 0x9d, 0x22, 0xe7, 0xae, 0xb1, 0xe9, 0xf3, 0x81, 0xf6, 0x8d, 0xd2, 0xd0, + 0x49, 0x40, 0x08, 0x08, 0xdc, 0x0d, 0x6b, 0x2a, 0x1d, 0xa4, 0x41, 0x42, 0xa5, 0xe8, 0xe7, 0x00, 0xfe, 0x61, 0x24, + 0x29, 0x00, 0xf6, 0x43, 0x35, 0x52, 0x44, 0x59, 0x16, 0xb8, 0x00, 0x34, 0xd7, 0x3e, 0xae, 0x84, 0x2f, 0x0c, 0x54, + 0x98, 0x9e, 0x66, 0xe5, 0xa5, 0x50, 0x22, 0xef, 0xd6, 0xa4, 0xac, 0x91, 0x4c, 0x3e, 0x45, 0x87, 0x4f, 0x79, 0xd7, + 0xaf, 0x25, 0x1e, 0xba, 0xe0, 0x29, 0x2c, 0xab, 0x7a, 0x7e, 0x15, 0x72, 0x72, 0xae, 0x41, 0x57, 0x48, 0xa1, 0xbf, + 0xe2, 0x24, 0xef, 0xbd, 0xf2, 0xab, 0x5a, 0x6a, 0x0c, 0x65, 0xef, 0xd7, 0x35, 0xc3, 0xf2, 0x72, 0x5e, 0x85, 0x29, + 0x08, 0xb8, 0x25, 0x4b, 0x82, 0xa5, 0xd4, 0x10, 0x60, 0x61, 0x7b, 0xa4, 0x95, 0x82, 0xbc, 0xd4, 0xe1, 0x9d, 0xa7, + 0x60, 0x05, 0x18, 0x87, 0x5a, 0x2a, 0x99, 0x46, 0x12, 0x5f, 0x2a, 0x51, 0x60, 0xca, 0xfd, 0x11, 0xf8, 0xa9, 0xcd, + 0x93, 0xae, 0x73, 0xd7, 0x8f, 0x67, 0x98, 0xda, 0x43, 0xa0, 0xc7, 0xde, 0x1d, 0x30, 0x25, 0xea, 0x3a, 0xac, 0x20, + 0x0e, 0xcd, 0x6a, 0x9a, 0x05, 0xcc, 0x98, 0x36, 0x68, 0xc9, 0x36, 0xd8, 0x72, 0x39, 0xd8, 0x47, 0x62, 0x7b, 0x56, + 0x2b, 0x20, 0x74, 0x0d, 0x1a, 0x18, 0x72, 0x97, 0x0a, 0x2d, 0xcc, 0x7b, 0x5d, 0x2a, 0xc2, 0xfd, 0x39, 0xe0, 0xd2, + 0x0a, 0xce, 0xbc, 0x8c, 0x06, 0xde, 0x8f, 0x8f, 0x13, 0x4c, 0x7c, 0x41, 0xac, 0xc0, 0x0e, 0x0e, 0x3a, 0xcd, 0xa6, + 0xc0, 0xa9, 0xb8, 0x48, 0x19, 0x2c, 0x2b, 0x4a, 0x6d, 0xf8, 0x21, 0x45, 0xb6, 0xee, 0xf2, 0x40, 0x77, 0x21, 0x16, + 0xc0, 0x4e, 0xbf, 0x60, 0xe4, 0x5b, 0xd6, 0xcb, 0x80, 0xc1, 0xa9, 0xd6, 0x38, 0x08, 0xfc, 0xe6, 0x66, 0x32, 0x2c, + 0x53, 0x62, 0xbb, 0x26, 0xab, 0x0b, 0xc8, 0x61, 0xa8, 0x26, 0xee, 0x20, 0x2c, 0x95, 0x3d, 0x5e, 0x94, 0x33, 0x5c, + 0x2e, 0x65, 0x21, 0x37, 0xcf, 0xab, 0x69, 0x3e, 0xb7, 0xd2, 0x6c, 0x3a, 0xde, 0x8a, 0x2f, 0x0a, 0xfe, 0x81, 0x13, + 0x4b, 0xab, 0x9e, 0x52, 0x2b, 0x3c, 0xca, 0xdc, 0x92, 0x75, 0x4a, 0x6a, 0x75, 0xdd, 0x40, 0x35, 0xc2, 0xd3, 0x34, + 0x6c, 0x04, 0x42, 0x4c, 0x70, 0xf1, 0xeb, 0x26, 0x13, 0xd3, 0xde, 0x12, 0x52, 0x47, 0xd8, 0x3d, 0x94, 0x13, 0xdc, + 0xd5, 0x3c, 0xfb, 0x3c, 0x9c, 0x5f, 0xcd, 0xdc, 0x7b, 0x06, 0x73, 0x3f, 0x0e, 0xb9, 0xc1, 0xe8, 0xb1, 0x4c, 0xf8, + 0x91, 0xb1, 0x8f, 0x5c, 0x55, 0x3d, 0x39, 0x09, 0x2b, 0x91, 0x25, 0x9e, 0x8c, 0xa3, 0x0e, 0xe3, 0x54, 0xb4, 0x26, + 0xc8, 0x2e, 0x2f, 0x0b, 0x73, 0x2f, 0x50, 0xd0, 0xd4, 0xe3, 0xf5, 0x38, 0x6d, 0xc5, 0xce, 0x46, 0x24, 0x72, 0xef, + 0x55, 0x2d, 0x12, 0x59, 0xf1, 0x39, 0x8e, 0x74, 0xc5, 0x41, 0xee, 0x93, 0x93, 0xd5, 0x4d, 0x2a, 0x74, 0x8b, 0x46, + 0xdb, 0xd8, 0xa3, 0xfa, 0x40, 0x52, 0xcf, 0xa8, 0xc0, 0xaa, 0xc6, 0xbe, 0x7b, 0xb7, 0x23, 0xd2, 0x2d, 0x95, 0x62, + 0x83, 0xa5, 0x85, 0xd1, 0x8c, 0x51, 0x30, 0x28, 0x29, 0x32, 0x50, 0xa3, 0xfc, 0x0a, 0xc1, 0xb0, 0x47, 0x0d, 0x40, + 0x71, 0xae, 0xaf, 0x7e, 0x5c, 0x4a, 0xb6, 0x10, 0x90, 0xb8, 0x4b, 0x06, 0x62, 0x4d, 0x30, 0x33, 0xf2, 0xc9, 0x7b, + 0xe0, 0xbc, 0x01, 0x43, 0x1f, 0x01, 0xfc, 0x02, 0xb1, 0xe9, 0xc1, 0xc4, 0xb6, 0x89, 0x28, 0xfa, 0x6c, 0xe0, 0x39, + 0x00, 0x3b, 0xaf, 0x42, 0xa3, 0xef, 0xaa, 0x14, 0x30, 0x64, 0x03, 0x37, 0x60, 0x55, 0x58, 0x6e, 0xef, 0x39, 0xb8, + 0x0d, 0xf0, 0xfa, 0x4c, 0x36, 0xdf, 0xc0, 0x3c, 0xc1, 0xea, 0xec, 0xc2, 0xaf, 0x2c, 0x6b, 0x71, 0xee, 0x74, 0xd0, + 0xa8, 0x57, 0x94, 0x10, 0xb5, 0xfb, 0x58, 0x7b, 0x80, 0x11, 0x16, 0xf1, 0xfe, 0x0a, 0xdf, 0xf5, 0xb8, 0xe5, 0x9e, + 0x46, 0x8b, 0x30, 0x5d, 0x25, 0x8d, 0x41, 0xc9, 0xba, 0x9f, 0x8c, 0xb8, 0x97, 0xfb, 0x22, 0x16, 0x5c, 0xe1, 0xc8, + 0xaa, 0x90, 0x62, 0x03, 0x49, 0x7a, 0xda, 0xa3, 0x03, 0xf6, 0x8d, 0x66, 0x2f, 0xa0, 0xcc, 0xfb, 0x8a, 0x54, 0x12, + 0x52, 0x9a, 0xdd, 0x10, 0x49, 0xc2, 0x5a, 0x91, 0xa7, 0xce, 0xfb, 0x8e, 0xf6, 0xb9, 0x95, 0x44, 0x30, 0x82, 0x93, + 0x30, 0x1d, 0x2b, 0x0f, 0x9a, 0x02, 0x5c, 0x45, 0x47, 0x4c, 0xdf, 0x04, 0xe4, 0x37, 0x03, 0xb9, 0xbd, 0x92, 0x5c, + 0x9b, 0x6b, 0x18, 0x9e, 0x20, 0xc1, 0xaa, 0x48, 0x04, 0x1e, 0x51, 0x03, 0x8e, 0xf9, 0x3a, 0xcf, 0x03, 0x4c, 0xf8, + 0xda, 0xde, 0x04, 0x80, 0x72, 0x72, 0x55, 0x9c, 0x95, 0x40, 0x37, 0x60, 0xb9, 0x3e, 0x4e, 0x8d, 0x8a, 0xc4, 0xc5, + 0x8d, 0xe9, 0xea, 0x96, 0xfe, 0x0c, 0x2d, 0x67, 0x32, 0xc4, 0x74, 0x10, 0x04, 0x64, 0xea, 0x3b, 0xe6, 0x08, 0x99, + 0x2b, 0xac, 0xcf, 0xb9, 0x53, 0x9b, 0xba, 0xc7, 0xa8, 0x9b, 0x27, 0xa9, 0xc5, 0xeb, 0xb4, 0x29, 0x25, 0x62, 0x52, + 0x62, 0x6e, 0x88, 0x54, 0x6c, 0xa6, 0xc4, 0x9d, 0x5b, 0xdf, 0x68, 0x21, 0x6d, 0xb4, 0x0d, 0x91, 0x83, 0xcd, 0x2a, + 0x79, 0x4f, 0x60, 0x3c, 0x17, 0x84, 0x2f, 0x5f, 0x51, 0x92, 0x0e, 0x73, 0x4c, 0x04, 0xab, 0x17, 0x53, 0x91, 0xbf, + 0x73, 0x74, 0x9a, 0xbd, 0x41, 0x0f, 0x52, 0x6f, 0x20, 0x31, 0x6b, 0xe2, 0xbb, 0x90, 0x86, 0x3a, 0x42, 0xa0, 0x32, + 0xaa, 0x65, 0x3a, 0x4e, 0xac, 0xc2, 0x37, 0x82, 0xaf, 0xde, 0xea, 0xe3, 0x7c, 0xe3, 0xb9, 0xb1, 0x1a, 0x41, 0x0c, + 0xde, 0x42, 0x3e, 0xf4, 0xa4, 0x08, 0x07, 0xc2, 0xe5, 0x9b, 0x9b, 0xbd, 0x7c, 0x97, 0x57, 0x21, 0x92, 0x0a, 0xc6, + 0x18, 0x33, 0x8a, 0x71, 0x4f, 0xd4, 0xd4, 0x62, 0x0e, 0x03, 0xcb, 0xd6, 0x61, 0x8e, 0x07, 0x00, 0xd0, 0xd2, 0x94, + 0x5e, 0x35, 0x15, 0x2a, 0xcf, 0x73, 0x09, 0x9f, 0xea, 0x10, 0x55, 0x35, 0x7e, 0xbb, 0x3e, 0x03, 0x85, 0xe0, 0xbe, + 0xd3, 0xf1, 0xf0, 0x10, 0x02, 0x56, 0x51, 0xc8, 0x02, 0xbd, 0x41, 0x7b, 0x55, 0x22, 0x14, 0x33, 0x27, 0xeb, 0x31, + 0xc3, 0x49, 0x05, 0x5b, 0xa8, 0x84, 0xa5, 0xd2, 0x02, 0xbf, 0xda, 0x08, 0xcd, 0x53, 0xc6, 0xbd, 0x57, 0x15, 0xce, + 0xa0, 0x3f, 0x98, 0xb7, 0xca, 0xa8, 0x6f, 0x57, 0x4e, 0x64, 0x2a, 0x30, 0x71, 0x33, 0x4b, 0xed, 0xf7, 0xcb, 0x3a, + 0xed, 0xe7, 0x15, 0x72, 0x9f, 0x93, 0xe6, 0xeb, 0xdc, 0x42, 0xf3, 0xc9, 0x70, 0xbf, 0x52, 0x7e, 0x68, 0x61, 0xd4, + 0x94, 0x5f, 0x5e, 0x57, 0x7e, 0x85, 0xa7, 0xc2, 0x5b, 0xfd, 0x2e, 0x0a, 0x5d, 0xd4, 0xe7, 0x60, 0x08, 0xe9, 0x47, + 0x70, 0x0d, 0x0d, 0x1e, 0x14, 0xc9, 0x62, 0xb1, 0x76, 0x41, 0x5c, 0x1f, 0x73, 0xaa, 0x1d, 0xca, 0x18, 0x23, 0x9e, + 0x96, 0x1c, 0x24, 0x19, 0x1c, 0x8c, 0xdf, 0xc0, 0x80, 0x98, 0x94, 0x84, 0x74, 0x08, 0x9d, 0xb5, 0x99, 0x88, 0xca, + 0x5d, 0xbc, 0xd9, 0xb8, 0xac, 0x29, 0x14, 0x61, 0x27, 0x98, 0xa9, 0x94, 0x0a, 0x02, 0x69, 0xf2, 0xdd, 0xe9, 0xd4, + 0x82, 0xa1, 0x85, 0x6b, 0x2a, 0x20, 0xaf, 0xed, 0x7a, 0xd0, 0xe4, 0x3d, 0xc5, 0xd0, 0xd7, 0xa9, 0x11, 0x2f, 0x33, + 0xf8, 0x1a, 0x36, 0x7f, 0x4d, 0x94, 0xe4, 0x21, 0x13, 0xb1, 0x57, 0xf0, 0x89, 0x90, 0x4d, 0xc1, 0xce, 0x04, 0xfa, + 0xa1, 0x5d, 0xd9, 0x4b, 0x77, 0x8b, 0xca, 0xa5, 0x45, 0x63, 0x2b, 0x51, 0xb3, 0xe6, 0x87, 0xf1, 0x66, 0x0a, 0xfb, + 0xd9, 0xa3, 0x04, 0x02, 0xd2, 0x54, 0x4e, 0x52, 0xcd, 0x7b, 0x98, 0x0e, 0x01, 0x24, 0xd8, 0xfd, 0x04, 0x16, 0xfa, + 0x4d, 0x89, 0x09, 0x16, 0x55, 0x63, 0xb7, 0x39, 0x68, 0xcd, 0x39, 0x69, 0xbe, 0x39, 0x6a, 0xed, 0x4d, 0x65, 0x3d, + 0x63, 0x76, 0x80, 0x6d, 0xbb, 0x9b, 0xc5, 0x61, 0xba, 0xd9, 0x19, 0x1a, 0x82, 0x0b, 0x8f, 0xff, 0x93, 0x12, 0xd3, + 0x40, 0x72, 0xa9, 0x1b, 0x3f, 0xa1, 0x0e, 0xc3, 0xff, 0x96, 0xa4, 0x80, 0x07, 0xb5, 0xd5, 0x58, 0x71, 0xee, 0x15, + 0x47, 0xc9, 0x65, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0x8c, 0x89, 0x62, 0x9e, 0x13, 0x00, 0xa3, 0xd8, 0xfc, + 0x29, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4d, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0x4f, 0xe8, 0x48, 0x45, 0x65, 0x73, 0x12, + 0xf3, 0x6f, 0x0b, 0x30, 0xcd, 0x89, 0x0f, 0xf5, 0x5c, 0xc3, 0x50, 0x80, 0xaf, 0x6c, 0x28, 0x35, 0xdb, 0xe3, 0xdf, + 0x3b, 0xdb, 0x7d, 0x49, 0x14, 0xc1, 0x02, 0x0d, 0xba, 0x5c, 0x81, 0x2f, 0x60, 0x19, 0xdc, 0x92, 0x7e, 0x0a, 0xbe, + 0x97, 0x57, 0xc1, 0x67, 0xec, 0x7f, 0x01, 0x68, 0x55, 0x60, 0x40, 0xb9, 0xd3, 0x34, 0xac, 0x84, 0xb8, 0x44, 0x85, + 0x59, 0xc5, 0xf9, 0xe3, 0x3a, 0xaf, 0x9b, 0x96, 0x25, 0x06, 0xe5, 0xe7, 0xae, 0xe1, 0xc6, 0xf7, 0x1a, 0xf9, 0xe3, + 0x7b, 0xcf, 0x41, 0xb7, 0x13, 0x69, 0xef, 0xde, 0xcd, 0xef, 0x90, 0x85, 0x86, 0xf7, 0xc2, 0xe6, 0xd0, 0x16, 0xe9, + 0x92, 0xab, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x07, 0x0c, 0xc1, 0x63, 0xa7, 0x32, 0xf9, + 0x0c, 0x1b, 0x4d, 0xb1, 0x6b, 0x2e, 0x0c, 0x3e, 0x50, 0x95, 0x85, 0xe4, 0xc5, 0x3a, 0xd9, 0x9e, 0x9d, 0xc2, 0xf3, + 0xcb, 0xb8, 0x00, 0xea, 0x00, 0xfa, 0x15, 0x95, 0xc5, 0x06, 0x72, 0x71, 0x53, 0xd6, 0x7a, 0x45, 0xe3, 0xf1, 0xb5, + 0x5d, 0x58, 0x5d, 0x81, 0x4f, 0xa3, 0x74, 0x9c, 0x88, 0x49, 0xcc, 0xa4, 0xca, 0x35, 0xb9, 0x36, 0xba, 0x97, 0xb6, + 0x68, 0x9e, 0x0b, 0x09, 0x5e, 0x11, 0xb8, 0x21, 0xf4, 0x95, 0xbe, 0x5c, 0x6f, 0xa0, 0xe0, 0x51, 0x7b, 0x73, 0x11, + 0x4c, 0x4c, 0x3c, 0x66, 0x48, 0x4d, 0xbf, 0x0e, 0xa7, 0x56, 0x16, 0x2b, 0x0e, 0xbf, 0xce, 0x19, 0x6b, 0x28, 0x00, + 0xe2, 0x93, 0x07, 0x57, 0xbb, 0x49, 0xaf, 0x94, 0x76, 0x50, 0x1a, 0x21, 0xbe, 0xad, 0xf0, 0x75, 0x97, 0x8a, 0xaf, + 0x5c, 0x75, 0xef, 0x6b, 0xc6, 0x8c, 0x0b, 0x46, 0xcf, 0xf9, 0x2c, 0x69, 0x5c, 0xbb, 0xa1, 0xbb, 0x3a, 0x3f, 0x7a, + 0x3f, 0xc8, 0xbc, 0x85, 0x19, 0xb0, 0x09, 0xa8, 0x82, 0xe7, 0xde, 0x6b, 0xe3, 0x44, 0xf9, 0x3b, 0xf3, 0x88, 0x57, + 0x0e, 0xb3, 0xee, 0x24, 0xf9, 0xbb, 0xc1, 0x77, 0xc1, 0xd5, 0x2d, 0x8d, 0x13, 0xe4, 0xae, 0x3a, 0x41, 0x26, 0xca, + 0xcd, 0xf4, 0x86, 0xdb, 0xbb, 0xad, 0x40, 0x10, 0xa7, 0x62, 0xfa, 0xa8, 0x1c, 0xd7, 0x8f, 0x16, 0xa8, 0x54, 0x44, + 0x7c, 0xaa, 0x72, 0x57, 0xae, 0x4c, 0x0d, 0xf5, 0xb8, 0x4e, 0x66, 0xa1, 0x69, 0xd6, 0xe4, 0x52, 0x36, 0x3d, 0x46, + 0xa6, 0xd9, 0xa9, 0x36, 0xbf, 0x7b, 0xe5, 0x21, 0x1d, 0x43, 0x73, 0xb1, 0x56, 0x0b, 0xee, 0x77, 0x15, 0x85, 0x77, + 0xbd, 0xd8, 0x48, 0x65, 0xa8, 0x59, 0x8f, 0xa2, 0x8f, 0xe3, 0x36, 0x73, 0x79, 0x94, 0xfd, 0x59, 0x03, 0xc0, 0x74, + 0x84, 0x45, 0x77, 0xd3, 0x33, 0xf6, 0x04, 0x7a, 0x7a, 0x22, 0x83, 0x44, 0xaf, 0x75, 0xbe, 0x6a, 0x95, 0x58, 0xba, + 0x86, 0xc0, 0xee, 0x35, 0x19, 0xab, 0x92, 0x76, 0xab, 0xf5, 0xab, 0x79, 0x3e, 0x4f, 0xf9, 0x4a, 0x9e, 0x4f, 0xcd, + 0xa2, 0xbb, 0xd3, 0x76, 0xaf, 0x4f, 0x0d, 0x15, 0x73, 0xad, 0x6f, 0xf2, 0x3b, 0xa6, 0xeb, 0x60, 0xa8, 0x45, 0x90, + 0x59, 0xed, 0xaa, 0x67, 0x65, 0x39, 0xab, 0x67, 0x72, 0xcc, 0x84, 0x6f, 0x2a, 0xdd, 0x21, 0xba, 0x61, 0xaa, 0x66, + 0xfa, 0xb1, 0xb1, 0x2d, 0x64, 0x9b, 0xe7, 0x17, 0xe3, 0x1c, 0x28, 0x2d, 0xf7, 0x97, 0x09, 0xc3, 0x8f, 0x97, 0x97, + 0x3f, 0x0a, 0x39, 0x55, 0x75, 0xf4, 0x96, 0x2f, 0x75, 0xcf, 0x60, 0x56, 0x2a, 0x27, 0xe2, 0x98, 0xad, 0x1f, 0xbc, + 0xb9, 0x7b, 0x05, 0x2c, 0xc7, 0x80, 0xdd, 0x31, 0x73, 0x1a, 0x43, 0x55, 0x1b, 0xf8, 0x87, 0xf5, 0x83, 0xad, 0xdb, + 0xc3, 0x3f, 0x0c, 0x7e, 0x08, 0xae, 0x6d, 0x6c, 0x6c, 0xe3, 0xed, 0x5a, 0x22, 0xc8, 0x2b, 0x3c, 0xd0, 0xc7, 0xab, + 0x8f, 0x82, 0x96, 0xeb, 0xc4, 0xf6, 0xc0, 0xa1, 0xb0, 0x35, 0xc8, 0x37, 0x29, 0x93, 0x46, 0x8b, 0x82, 0x67, 0x33, + 0x39, 0x43, 0x21, 0xaf, 0xf9, 0x38, 0x68, 0x3b, 0xc2, 0xdf, 0xc0, 0xa9, 0x1d, 0x2f, 0x2f, 0x3f, 0x41, 0x1f, 0xf0, + 0x74, 0xa5, 0x34, 0x15, 0x71, 0x4a, 0xb9, 0x45, 0x57, 0xeb, 0x3c, 0x18, 0x29, 0x2e, 0xa6, 0xa8, 0x74, 0xdc, 0xe5, + 0xb5, 0xb3, 0x91, 0xd3, 0x5f, 0xe2, 0xd5, 0x45, 0xba, 0x7c, 0x24, 0xb2, 0x55, 0x4b, 0xef, 0x37, 0x7d, 0xba, 0x6d, + 0xcf, 0x18, 0x9f, 0x66, 0x63, 0x3a, 0x98, 0xf1, 0x71, 0x22, 0xbc, 0x3e, 0x31, 0xd6, 0x77, 0x8b, 0xc0, 0x74, 0x73, + 0x6c, 0xf2, 0xc3, 0xf1, 0x7a, 0xb3, 0x59, 0xe3, 0x0e, 0xde, 0x38, 0x4f, 0x9c, 0x65, 0x89, 0x11, 0x95, 0xa5, 0x86, + 0x07, 0xb4, 0x42, 0xdc, 0xbc, 0x67, 0x02, 0xe3, 0xb2, 0x0b, 0x92, 0xda, 0x6e, 0x20, 0x70, 0xb1, 0x27, 0x31, 0x4b, + 0xc6, 0xb6, 0x07, 0xe5, 0x81, 0xbe, 0x18, 0x4d, 0xb7, 0x80, 0x69, 0x79, 0xed, 0xec, 0x2c, 0xb5, 0xbd, 0x6a, 0xaa, + 0x00, 0x66, 0xc9, 0xf2, 0xf8, 0x04, 0x59, 0xf7, 0x5b, 0xe8, 0x22, 0x06, 0x8c, 0x8d, 0x2b, 0x73, 0xee, 0x72, 0xdd, + 0x8a, 0xf8, 0x46, 0x13, 0x69, 0x52, 0x1f, 0x52, 0xdf, 0x61, 0x58, 0xab, 0xab, 0x1c, 0x24, 0x70, 0x8f, 0xbc, 0x5b, + 0xe2, 0xd2, 0xd3, 0x67, 0x16, 0x93, 0x2a, 0x7d, 0x4b, 0x5d, 0x8b, 0x6b, 0x86, 0xbd, 0xe2, 0x01, 0xd8, 0x1f, 0x18, + 0xb7, 0x88, 0x45, 0xbc, 0x9d, 0xd7, 0x52, 0x58, 0x1b, 0x73, 0xa0, 0xb9, 0xe1, 0x06, 0xbf, 0xb1, 0x6a, 0xcd, 0xc0, + 0x0c, 0x33, 0xce, 0x48, 0x7e, 0x33, 0xee, 0x55, 0x4d, 0x1c, 0xb9, 0x0a, 0x20, 0xfa, 0x96, 0x74, 0x49, 0x0e, 0xaf, + 0x64, 0xb9, 0xea, 0x0c, 0xf9, 0x57, 0x58, 0x67, 0xbd, 0x38, 0x01, 0x33, 0x69, 0xca, 0x4b, 0x4c, 0x4c, 0x11, 0x97, + 0x9b, 0x65, 0xcc, 0xd3, 0xf4, 0x59, 0xb4, 0x83, 0x93, 0x1b, 0x09, 0x1c, 0xb1, 0x6f, 0x2c, 0x43, 0x33, 0x61, 0x23, + 0x26, 0xd2, 0xa8, 0x94, 0x12, 0x3e, 0x90, 0x4b, 0x2d, 0xf9, 0xcb, 0x5c, 0x5e, 0x7d, 0xb9, 0x4d, 0x70, 0x40, 0x5e, + 0x03, 0xcb, 0xa1, 0x71, 0xdc, 0x32, 0x90, 0x88, 0xc5, 0x80, 0x18, 0xb5, 0x2a, 0x57, 0x93, 0x51, 0x9d, 0xcc, 0x57, + 0xc8, 0x85, 0x8a, 0x3c, 0xb8, 0x25, 0x50, 0xf2, 0xe7, 0x98, 0x3a, 0x98, 0x95, 0xda, 0x4d, 0x8b, 0x4d, 0x92, 0xf7, + 0xcc, 0x80, 0xe4, 0xfa, 0x6b, 0x78, 0x68, 0xfc, 0xe2, 0x95, 0x39, 0x25, 0x7c, 0x51, 0xc6, 0xd2, 0xd2, 0x98, 0x4b, + 0xff, 0x42, 0xde, 0xa7, 0x95, 0x80, 0xfd, 0x0a, 0x62, 0xca, 0xc0, 0x25, 0x36, 0x2e, 0x48, 0xca, 0x6b, 0x79, 0xca, + 0xee, 0x6b, 0x28, 0xdf, 0x15, 0x93, 0xae, 0x52, 0x59, 0x57, 0x58, 0x75, 0xbf, 0x2e, 0x58, 0x7e, 0xb1, 0xcf, 0x30, + 0x37, 0x19, 0x0d, 0xb2, 0x15, 0x33, 0x9b, 0xf2, 0xab, 0xbd, 0x6b, 0xbf, 0xf2, 0x50, 0xd2, 0xa1, 0x5a, 0xa5, 0x9b, + 0x57, 0x6e, 0x38, 0xc6, 0x8d, 0x1b, 0x8e, 0x00, 0x36, 0x86, 0x9d, 0x2a, 0x52, 0xeb, 0xfc, 0xf7, 0xd5, 0xf0, 0x13, + 0xed, 0xb5, 0xa1, 0xde, 0x75, 0xc3, 0xb5, 0xe9, 0xe9, 0xd7, 0xa0, 0x6a, 0x64, 0x09, 0x5d, 0x87, 0x2a, 0x26, 0x23, + 0x51, 0x62, 0xba, 0x4a, 0x79, 0xd4, 0xd7, 0x88, 0x73, 0x10, 0x37, 0x94, 0xbf, 0xf8, 0xe7, 0xf0, 0xe2, 0x28, 0x40, + 0x23, 0x6a, 0x39, 0xc9, 0x52, 0xde, 0x9a, 0x44, 0xb3, 0x38, 0xb9, 0x08, 0x16, 0x71, 0x6b, 0x96, 0xa5, 0x59, 0x31, + 0x07, 0xae, 0xf4, 0x8a, 0x0b, 0xb0, 0xe1, 0x67, 0xad, 0x45, 0xec, 0x3d, 0x67, 0xc9, 0x29, 0xe3, 0xf1, 0x28, 0xf2, + 0xec, 0xbd, 0x1c, 0xc4, 0x83, 0xf5, 0x3a, 0xca, 0xf3, 0xec, 0xcc, 0xf6, 0xde, 0x65, 0xc7, 0xc0, 0xb4, 0xde, 0x9b, + 0xf3, 0x8b, 0x13, 0x96, 0x7a, 0xef, 0x8f, 0x17, 0x29, 0x5f, 0x78, 0x45, 0x94, 0x16, 0xad, 0x82, 0xe5, 0xf1, 0x04, + 0xd4, 0x44, 0x92, 0xe5, 0x2d, 0xcc, 0x7f, 0x9e, 0xb1, 0x20, 0x89, 0x4f, 0xa6, 0xdc, 0x1a, 0x47, 0xf9, 0xa7, 0x5e, + 0xab, 0x35, 0xcf, 0xe3, 0x59, 0x94, 0x5f, 0xb4, 0xa8, 0x45, 0xf0, 0x45, 0x7b, 0x3b, 0xfa, 0x6a, 0x72, 0xbf, 0xc7, + 0x73, 0xe8, 0x1b, 0x23, 0x15, 0x03, 0x10, 0x3e, 0xd6, 0xf6, 0x4e, 0x7b, 0x56, 0xdc, 0x11, 0x27, 0x4a, 0x51, 0xca, + 0xcb, 0x23, 0xef, 0x23, 0x03, 0xb8, 0xfd, 0x63, 0x9e, 0x7a, 0xe0, 0xcb, 0xf1, 0x2c, 0x5d, 0x8e, 0x16, 0x79, 0x01, + 0x03, 0xcc, 0xb3, 0x38, 0xe5, 0x2c, 0xef, 0x1d, 0x67, 0x39, 0x90, 0xad, 0x95, 0x47, 0xe3, 0x78, 0x51, 0x04, 0xf7, + 0xe7, 0xe7, 0x3d, 0xb4, 0x15, 0x4e, 0xf2, 0x6c, 0x91, 0x8e, 0xe5, 0x5c, 0x71, 0x0a, 0x1b, 0x23, 0xe6, 0x66, 0x05, + 0x7d, 0x09, 0x05, 0xe0, 0x4b, 0x59, 0x94, 0xb7, 0x4e, 0xb0, 0x33, 0x1a, 0xfa, 0xed, 0x31, 0x3b, 0xf1, 0xf2, 0x93, + 0xe3, 0xc8, 0xe9, 0x74, 0x1f, 0x7a, 0xea, 0x9f, 0xbf, 0xe3, 0x82, 0xe1, 0xbe, 0xb6, 0xb8, 0xd3, 0x6e, 0xff, 0x9d, + 0xdb, 0x6b, 0xcc, 0x42, 0x00, 0x05, 0x9d, 0xf9, 0xb9, 0x55, 0x64, 0x09, 0xac, 0xcf, 0xba, 0x9e, 0xbd, 0x39, 0xf8, + 0x4d, 0x71, 0x7a, 0x12, 0x74, 0xe7, 0xe7, 0x25, 0x62, 0x17, 0x88, 0x84, 0x4c, 0x89, 0xa4, 0x7c, 0x5b, 0xfe, 0x5e, + 0x88, 0x1f, 0xad, 0x87, 0xb8, 0xab, 0x20, 0xae, 0xa8, 0xde, 0x1a, 0xc3, 0x3e, 0x20, 0xf2, 0x77, 0x0a, 0x01, 0xc8, + 0x14, 0x9c, 0xc0, 0x5c, 0xc1, 0x41, 0x2f, 0xbf, 0x1b, 0x8c, 0xee, 0x7a, 0x30, 0x1e, 0xdd, 0x04, 0x46, 0x9e, 0x8e, + 0x97, 0xf5, 0x75, 0xed, 0x80, 0x73, 0xda, 0x9b, 0x32, 0xe4, 0xa7, 0xa0, 0x8b, 0xcf, 0x67, 0xf1, 0x98, 0x4f, 0xc5, + 0x23, 0xb1, 0xf3, 0x99, 0xa8, 0xdb, 0x69, 0xb7, 0xc5, 0x7b, 0x01, 0x0a, 0x2d, 0xe8, 0xf8, 0xd8, 0x00, 0x98, 0xe8, + 0xc3, 0x55, 0x1f, 0xb1, 0xf9, 0xfa, 0xc6, 0x2f, 0xd5, 0x78, 0x67, 0x2a, 0x6f, 0x50, 0xa8, 0x08, 0xf5, 0xcd, 0x16, + 0xcc, 0x78, 0xcb, 0xfb, 0x1d, 0x7d, 0x50, 0x35, 0xf8, 0x9a, 0x91, 0xd6, 0x0b, 0xb8, 0x67, 0xe6, 0x02, 0xf5, 0xd2, + 0x3e, 0x86, 0xa4, 0x5a, 0x2d, 0x17, 0xf4, 0x06, 0xc3, 0x10, 0x12, 0x1d, 0x08, 0x3a, 0xf9, 0xa0, 0xa0, 0x6f, 0x6a, + 0x64, 0x6e, 0x50, 0x38, 0x99, 0x0b, 0x5b, 0x3e, 0xd3, 0x72, 0x1d, 0x94, 0x34, 0x78, 0xd9, 0x1f, 0x98, 0x6c, 0x00, + 0xd2, 0x9b, 0x42, 0x5d, 0x7f, 0x09, 0x85, 0x2b, 0xa5, 0x1c, 0xa9, 0xd9, 0x75, 0x57, 0xf4, 0x61, 0x55, 0x62, 0xca, + 0x48, 0x3e, 0x1c, 0xfe, 0x3b, 0x0c, 0x7b, 0x47, 0x3b, 0x96, 0x45, 0xb6, 0xc8, 0x47, 0x14, 0xa9, 0x5b, 0xf5, 0xf8, + 0x6d, 0x52, 0xb8, 0xb6, 0xc7, 0xb4, 0x9c, 0x47, 0x37, 0xb8, 0xf6, 0x91, 0x03, 0x4e, 0x87, 0x20, 0xe2, 0x8e, 0x81, + 0x8c, 0x72, 0x28, 0x08, 0x51, 0x75, 0x8d, 0x29, 0xdf, 0x8d, 0xee, 0x5f, 0xfa, 0x8b, 0x34, 0x06, 0x49, 0xf7, 0x31, + 0x1e, 0xd3, 0xbd, 0x93, 0x78, 0x4c, 0x07, 0x11, 0x2d, 0x4a, 0x3c, 0xc2, 0xc8, 0x36, 0x14, 0xa8, 0xef, 0xb0, 0xc0, + 0xb3, 0x4c, 0x64, 0xb1, 0x5b, 0x36, 0x1e, 0x26, 0x18, 0xaa, 0x72, 0x9c, 0xcd, 0xa2, 0x38, 0x0d, 0xf0, 0xfb, 0x20, + 0x9e, 0x1e, 0x31, 0xc0, 0x2e, 0x1e, 0xfc, 0x64, 0x32, 0x17, 0xad, 0xe3, 0xfa, 0xbf, 0x80, 0x1c, 0xa1, 0xfe, 0xa5, + 0xf4, 0xc3, 0x34, 0x5c, 0xea, 0x98, 0xb7, 0x5e, 0x0a, 0xb2, 0x87, 0x2b, 0x9b, 0x95, 0x51, 0x9c, 0x63, 0x97, 0xd3, + 0x8f, 0x41, 0xab, 0x13, 0x74, 0xb4, 0xeb, 0x5a, 0xbb, 0x8d, 0x2a, 0x72, 0x59, 0xe4, 0x8d, 0x46, 0x82, 0x41, 0x3f, + 0x0b, 0x38, 0xab, 0x77, 0x0d, 0xab, 0x27, 0xf9, 0x12, 0x03, 0x38, 0x27, 0xa9, 0x53, 0x03, 0x82, 0xce, 0x02, 0xae, + 0x98, 0xca, 0x2d, 0x23, 0x52, 0x4a, 0x8f, 0x69, 0x03, 0xd7, 0xef, 0x12, 0xe1, 0xbd, 0xa1, 0x7a, 0x0a, 0x94, 0x62, + 0xb9, 0xf1, 0xd1, 0xae, 0xd8, 0xf1, 0x16, 0xf1, 0x58, 0x68, 0xc3, 0x16, 0xb4, 0xad, 0x3f, 0x8d, 0x80, 0x4a, 0x9f, + 0x42, 0x7b, 0x63, 0xe9, 0xa8, 0xc4, 0xfa, 0x1c, 0xe6, 0xda, 0x13, 0x5a, 0x8f, 0x6e, 0xe4, 0xdb, 0xfd, 0x8d, 0x25, + 0x2f, 0x77, 0xb7, 0x44, 0xef, 0xfe, 0x51, 0x59, 0x90, 0x82, 0x32, 0x03, 0x69, 0xd5, 0x14, 0xa2, 0x0e, 0x86, 0xa5, + 0xf4, 0x5d, 0x1c, 0x37, 0xd7, 0x46, 0x97, 0x88, 0x18, 0x4b, 0xb6, 0x2b, 0x30, 0x5d, 0x29, 0xca, 0x61, 0x4f, 0xea, + 0x84, 0x94, 0x42, 0xe4, 0x60, 0xf4, 0x56, 0xa1, 0x38, 0x42, 0x08, 0x06, 0x1b, 0xcb, 0xb8, 0x0c, 0x37, 0x96, 0x59, + 0x79, 0x04, 0x96, 0x09, 0x42, 0x95, 0xab, 0xcf, 0xbb, 0xc0, 0xc4, 0x22, 0xc8, 0x62, 0xd1, 0x08, 0x38, 0x2d, 0x2b, + 0x6d, 0x6b, 0x20, 0xa0, 0x01, 0x0f, 0x10, 0x0b, 0xc0, 0x76, 0xa3, 0x5e, 0x0c, 0x70, 0x11, 0xad, 0xfb, 0x30, 0xd0, + 0xee, 0x96, 0x68, 0x04, 0x78, 0xe5, 0x08, 0x72, 0x85, 0x16, 0xa6, 0xe3, 0x98, 0xa8, 0x8d, 0xe3, 0x53, 0x4d, 0x3a, + 0xca, 0x4d, 0xee, 0xef, 0x26, 0xd1, 0x31, 0x4b, 0x60, 0xc8, 0xe2, 0xf2, 0xb2, 0x0d, 0x23, 0x89, 0x57, 0x6b, 0x37, + 0x4e, 0xe7, 0x0b, 0xf9, 0x99, 0x2d, 0x98, 0xb8, 0x83, 0x27, 0x9f, 0x78, 0x0b, 0x60, 0xa0, 0x8e, 0xf2, 0x02, 0x39, + 0x00, 0x80, 0x48, 0xa7, 0x08, 0x08, 0x5d, 0xc5, 0x16, 0x50, 0x1a, 0x8f, 0x57, 0xcb, 0x60, 0x27, 0xce, 0xb1, 0x34, + 0x85, 0xe7, 0x59, 0x9c, 0xe2, 0x63, 0x81, 0x8f, 0xd1, 0x39, 0x3e, 0x66, 0xf0, 0xa8, 0x71, 0xcf, 0x4b, 0xfb, 0x6f, + 0xb2, 0x02, 0x96, 0x26, 0x40, 0x76, 0x79, 0x09, 0xf2, 0x5e, 0x93, 0x60, 0x77, 0x0b, 0x88, 0x85, 0x9c, 0x22, 0x3e, + 0xba, 0xc2, 0x4c, 0x32, 0xb2, 0x62, 0xde, 0x12, 0xe5, 0x16, 0x69, 0xd5, 0x10, 0x9c, 0xae, 0xdc, 0x69, 0x18, 0x0f, + 0x9e, 0x4c, 0x2f, 0x79, 0x82, 0x2f, 0xae, 0x6d, 0x89, 0xaf, 0x62, 0x08, 0xa2, 0xd0, 0x23, 0x62, 0xa8, 0xcb, 0xb8, + 0xfc, 0xde, 0x4d, 0x1c, 0xda, 0x38, 0x0b, 0xd8, 0x6f, 0xa8, 0x05, 0x78, 0x14, 0x27, 0xa2, 0xf1, 0x1a, 0x7c, 0x1a, + 0x79, 0x82, 0x84, 0xce, 0xee, 0x56, 0x05, 0x1b, 0x00, 0x3f, 0x12, 0xb7, 0xac, 0x1d, 0x91, 0x03, 0x69, 0x8b, 0x72, + 0x3a, 0x3b, 0x97, 0x5b, 0x5a, 0x46, 0x76, 0x45, 0xac, 0x5c, 0xa3, 0x4a, 0x39, 0x8b, 0xf6, 0x24, 0x4a, 0xd7, 0x35, + 0x05, 0xe8, 0xe7, 0x8c, 0x8d, 0x3d, 0xdb, 0x02, 0x59, 0x2a, 0x9e, 0x3f, 0x26, 0xec, 0x94, 0xc9, 0x2f, 0xa5, 0xe8, + 0x41, 0x74, 0xe5, 0x08, 0x54, 0x32, 0x97, 0x97, 0x38, 0x25, 0x7b, 0x2a, 0x1c, 0x25, 0x25, 0xea, 0x88, 0x78, 0xb6, + 0x31, 0x68, 0x73, 0x8e, 0x76, 0x7d, 0x58, 0xaf, 0x03, 0xd6, 0xae, 0x2d, 0xe0, 0x25, 0x3b, 0xee, 0x66, 0xe4, 0x60, + 0x00, 0x36, 0x99, 0xc0, 0x76, 0x51, 0x91, 0x65, 0x2d, 0x0b, 0x04, 0x54, 0xe0, 0x94, 0x7a, 0xb6, 0x68, 0x61, 0x57, + 0x6d, 0xf5, 0x93, 0x24, 0x4e, 0x92, 0x8d, 0x3e, 0xad, 0x99, 0x0b, 0xb8, 0x63, 0x43, 0x44, 0x5a, 0x1b, 0xf2, 0xcd, + 0xfe, 0xb7, 0x7f, 0xfe, 0x9f, 0xff, 0x15, 0x06, 0xa6, 0x7e, 0x6e, 0x69, 0x5d, 0xdd, 0xea, 0x7f, 0x40, 0xab, 0x45, + 0x7a, 0x43, 0xbb, 0xbf, 0xfe, 0xe3, 0x7f, 0x83, 0x66, 0x74, 0x23, 0xc7, 0x2d, 0x8f, 0x08, 0xa2, 0x11, 0x1a, 0x41, + 0x9f, 0x05, 0x52, 0x6d, 0x90, 0x2b, 0x67, 0xfa, 0x27, 0x04, 0xbb, 0xe0, 0xd9, 0xfc, 0x5a, 0x70, 0x10, 0xea, 0x51, + 0x92, 0x15, 0x4c, 0xc3, 0x23, 0x64, 0xed, 0xe7, 0x01, 0x44, 0x73, 0xcd, 0x81, 0xcb, 0x0b, 0x4b, 0x8f, 0x23, 0x96, + 0x67, 0xdd, 0x38, 0x8d, 0xd5, 0x2b, 0x18, 0x27, 0x74, 0x28, 0xae, 0x00, 0xeb, 0x25, 0x9e, 0xe0, 0x81, 0x04, 0x82, + 0x5b, 0xff, 0xca, 0xd7, 0xfa, 0xc1, 0x34, 0x7f, 0x8a, 0xb1, 0x44, 0x28, 0x45, 0x8d, 0x00, 0x3f, 0x41, 0x68, 0x7d, + 0xd4, 0xcf, 0xd1, 0xb9, 0x7e, 0x46, 0xc1, 0x26, 0x26, 0x00, 0x5d, 0x34, 0x43, 0x33, 0xc3, 0x9c, 0x41, 0xa4, 0x01, + 0x54, 0x7e, 0xa4, 0x91, 0x4d, 0x22, 0x84, 0xd7, 0x47, 0x4c, 0xba, 0xc4, 0x2b, 0x36, 0x8b, 0x9c, 0x7d, 0x4c, 0xb2, + 0x33, 0x0c, 0x4e, 0x21, 0x91, 0xae, 0xaa, 0x2f, 0xff, 0xf5, 0x5f, 0x7c, 0xff, 0x5f, 0xff, 0xe5, 0x8a, 0x06, 0x53, + 0xd8, 0x07, 0x60, 0x4d, 0xf2, 0x50, 0xd3, 0xb9, 0x81, 0xd6, 0xfa, 0x41, 0x11, 0xcf, 0xf5, 0x35, 0x12, 0x71, 0x2c, + 0x95, 0x78, 0xcb, 0x47, 0x42, 0x5b, 0x33, 0xc5, 0xcd, 0xb3, 0x20, 0x64, 0x57, 0x4c, 0x83, 0x55, 0x37, 0xcc, 0x73, + 0xe4, 0x06, 0xd7, 0xd0, 0xe5, 0x33, 0x31, 0x5e, 0x0f, 0xc6, 0x8d, 0x10, 0x78, 0x20, 0xfd, 0x85, 0x7e, 0x78, 0x22, + 0xa4, 0x7b, 0x20, 0x96, 0x41, 0xca, 0xfa, 0x1a, 0x40, 0x9e, 0x75, 0x40, 0x13, 0x50, 0x93, 0xb8, 0xd2, 0xad, 0x64, + 0x8a, 0x1c, 0xe7, 0xfd, 0x57, 0x78, 0xab, 0xce, 0xc2, 0xde, 0xa8, 0x59, 0x0b, 0x32, 0x04, 0x38, 0x19, 0x02, 0x52, + 0x03, 0x99, 0x3a, 0x18, 0x3d, 0x8c, 0x6c, 0xbd, 0xae, 0xfd, 0x88, 0xdd, 0x6b, 0xda, 0x92, 0x4b, 0x6d, 0x19, 0x4b, + 0x4b, 0x56, 0x6a, 0x4b, 0xfc, 0x76, 0x4a, 0x43, 0x5b, 0xc6, 0x57, 0x6a, 0x4b, 0xa4, 0xdc, 0x00, 0x47, 0x0e, 0xed, + 0x4d, 0x0c, 0xa3, 0x18, 0xba, 0x99, 0xa3, 0x5d, 0x02, 0xbe, 0xf3, 0xe8, 0x93, 0x34, 0x4b, 0x08, 0x01, 0x8c, 0x23, + 0x68, 0x43, 0x4b, 0x60, 0x00, 0x2a, 0xf6, 0xa8, 0xd4, 0x9b, 0x1e, 0x1f, 0x8d, 0x09, 0xb8, 0xbb, 0x9c, 0x30, 0x14, + 0xc9, 0x47, 0x5b, 0x38, 0x84, 0xd8, 0x2b, 0x25, 0x3d, 0x03, 0x52, 0x5b, 0x38, 0xce, 0x91, 0xb7, 0x14, 0x01, 0xa9, + 0xc0, 0x7e, 0xfb, 0x66, 0xff, 0xc0, 0xf6, 0x8e, 0xb3, 0xf1, 0x45, 0x60, 0x83, 0x33, 0x01, 0x86, 0x87, 0xeb, 0xf3, + 0x29, 0x4b, 0x1d, 0x65, 0xce, 0x67, 0x09, 0xb8, 0x33, 0xd9, 0x89, 0xf8, 0x7e, 0x42, 0x33, 0x98, 0x0e, 0x34, 0xa5, + 0x0f, 0x2c, 0xf6, 0x77, 0xb9, 0xf8, 0xf6, 0x28, 0xcf, 0xf1, 0xb1, 0x8f, 0xe9, 0x04, 0xbb, 0x5b, 0xf0, 0x80, 0x2f, + 0xfb, 0xa8, 0x8a, 0xf4, 0x9b, 0x80, 0xb3, 0x10, 0xef, 0x5b, 0xd8, 0x7e, 0x4b, 0xf5, 0x45, 0x28, 0xfa, 0x92, 0xd1, + 0xb4, 0xc5, 0x5d, 0x99, 0x71, 0x34, 0xf6, 0x18, 0xad, 0x34, 0xb2, 0xb8, 0x81, 0x1a, 0x7c, 0xac, 0x4b, 0x84, 0xe6, + 0x37, 0x8a, 0x68, 0x94, 0x4a, 0x4f, 0xcb, 0x2a, 0x9c, 0x90, 0x2c, 0x3b, 0x31, 0x19, 0xfc, 0x24, 0xf0, 0x8f, 0xcc, + 0x6f, 0x85, 0x89, 0xcf, 0xfa, 0x68, 0x24, 0x0f, 0xff, 0xec, 0x7d, 0x64, 0xde, 0xc5, 0x11, 0xb5, 0x54, 0x8e, 0x29, + 0x46, 0x4c, 0xd0, 0x81, 0x6f, 0xab, 0x08, 0x04, 0x98, 0x26, 0x49, 0x34, 0x2f, 0x58, 0xa0, 0x1e, 0xa4, 0x8f, 0x8a, + 0xae, 0xee, 0x6a, 0x50, 0xc0, 0x34, 0x61, 0x4a, 0x3e, 0x5d, 0x9a, 0x4e, 0xec, 0x03, 0x70, 0x62, 0x31, 0x01, 0xbf, + 0x15, 0x81, 0xe2, 0x4d, 0x83, 0x84, 0x4d, 0x78, 0xc9, 0xf1, 0x86, 0xf7, 0x52, 0x45, 0x0d, 0xfc, 0xee, 0x0e, 0x38, + 0xb6, 0x96, 0x8f, 0xff, 0xdf, 0x34, 0xf6, 0x38, 0x48, 0xc1, 0x11, 0xa5, 0x2b, 0x1f, 0x78, 0x9d, 0x0e, 0x20, 0x32, + 0xdf, 0x97, 0xc6, 0x44, 0x23, 0x86, 0x11, 0x95, 0x92, 0xe7, 0x20, 0xb2, 0x3d, 0x9e, 0x9b, 0xed, 0x40, 0xd4, 0xae, + 0x84, 0x55, 0x56, 0x1d, 0xfb, 0x6d, 0x57, 0x9a, 0xff, 0xab, 0x8d, 0x55, 0x74, 0xa4, 0xfe, 0xb6, 0x42, 0x21, 0x23, + 0x8e, 0x53, 0x0a, 0x2d, 0xb3, 0x14, 0x3d, 0x4c, 0x9c, 0x56, 0x23, 0x3c, 0x37, 0x1a, 0x89, 0x25, 0xed, 0xf8, 0x43, + 0xda, 0xf1, 0x24, 0xc1, 0x86, 0x4b, 0x31, 0xf7, 0x28, 0x4a, 0x46, 0x0e, 0x02, 0x60, 0xb5, 0xac, 0x47, 0x40, 0x4d, + 0x57, 0x45, 0x19, 0xfc, 0x87, 0x48, 0xdc, 0x52, 0xc8, 0xbb, 0x35, 0x54, 0x3a, 0x1a, 0x96, 0x65, 0xef, 0x8c, 0x39, + 0x87, 0xbf, 0xc9, 0x0b, 0x03, 0xe2, 0x9e, 0xa8, 0xfe, 0xd6, 0x5e, 0xbb, 0x74, 0x87, 0xde, 0x5f, 0x8c, 0x0f, 0x98, + 0xd9, 0x8a, 0xa1, 0x6d, 0x0f, 0x96, 0xe1, 0x2f, 0x21, 0xf6, 0x7d, 0xe5, 0xd8, 0x68, 0x55, 0x52, 0xcd, 0x45, 0x8b, + 0xf8, 0xcb, 0xc6, 0x6e, 0x22, 0xdc, 0xfd, 0xfd, 0x55, 0x51, 0x8b, 0x6f, 0x6e, 0x8e, 0x5a, 0xb0, 0x5b, 0x46, 0x2d, + 0xbe, 0xf9, 0x83, 0xa3, 0x16, 0xdf, 0x37, 0xa3, 0x16, 0xbf, 0x7e, 0x4e, 0xd4, 0x22, 0xcf, 0xce, 0x8a, 0xb0, 0x23, + 0x4f, 0xc9, 0x41, 0xe6, 0xfc, 0x6d, 0xc2, 0x17, 0x30, 0x51, 0x23, 0x78, 0x41, 0xd1, 0x0a, 0x91, 0xd8, 0x07, 0x92, + 0x5d, 0xc6, 0x0a, 0xda, 0x3a, 0x83, 0xae, 0x75, 0x5f, 0x5d, 0x19, 0x02, 0x6f, 0xcd, 0xd5, 0x97, 0xdd, 0xba, 0x2a, + 0x9a, 0x10, 0xd0, 0x37, 0x3f, 0x75, 0xc7, 0xee, 0xa6, 0x4a, 0xdd, 0x32, 0x47, 0xe8, 0xa9, 0x88, 0xbc, 0x60, 0x9f, + 0xa5, 0xfd, 0x9f, 0x0e, 0x3b, 0xbd, 0xed, 0xce, 0x0c, 0x7a, 0x83, 0x0e, 0x85, 0xb7, 0x76, 0x6f, 0x7b, 0x1b, 0xdf, + 0xce, 0xd4, 0x5b, 0x17, 0xdf, 0x62, 0xf5, 0xb6, 0x83, 0x6f, 0x23, 0xf5, 0xf6, 0x00, 0xdf, 0xc6, 0xea, 0xed, 0x21, + 0xbe, 0x9d, 0xda, 0xe5, 0x21, 0xd7, 0xc0, 0x3d, 0x04, 0xbe, 0x22, 0x43, 0x3f, 0x50, 0x65, 0xb0, 0x69, 0xf1, 0xaa, + 0x5d, 0x74, 0x12, 0xc4, 0x9e, 0x70, 0x88, 0x82, 0xdc, 0x3b, 0x03, 0xc9, 0x1f, 0x50, 0x66, 0xd9, 0x53, 0xfc, 0xe6, + 0x02, 0xf8, 0x0f, 0x07, 0xf1, 0x8c, 0xa9, 0x8f, 0xcf, 0x2a, 0xac, 0xc1, 0x86, 0x3c, 0x6c, 0x0f, 0xcb, 0x9e, 0x5e, + 0x27, 0x11, 0x2c, 0x51, 0x27, 0xf7, 0xb4, 0x72, 0x55, 0x9d, 0x98, 0xae, 0xa5, 0x57, 0xf8, 0x0a, 0x3d, 0x62, 0xb8, + 0xd0, 0x13, 0xb0, 0x8d, 0x5a, 0xe7, 0xe0, 0x74, 0xad, 0xd5, 0x2d, 0x08, 0x91, 0xd6, 0x26, 0x84, 0x93, 0x7e, 0x3b, + 0x88, 0x4e, 0xf4, 0xf3, 0x2b, 0x30, 0x76, 0xa3, 0x13, 0x76, 0x93, 0x9e, 0x21, 0x10, 0x4d, 0x1d, 0xa3, 0x80, 0x20, + 0x6b, 0x08, 0x96, 0x06, 0x9d, 0x3f, 0xa9, 0x63, 0x90, 0x3a, 0x75, 0xad, 0x43, 0xd3, 0xd7, 0x8b, 0x80, 0xa2, 0x55, + 0xc1, 0x2e, 0xd8, 0xdc, 0x54, 0x2a, 0x28, 0x0c, 0x15, 0x58, 0x70, 0xad, 0x2a, 0xd2, 0xfe, 0xf1, 0x95, 0x0a, 0xc9, + 0x52, 0xba, 0xc8, 0x8c, 0xe4, 0xeb, 0x30, 0xfe, 0xaa, 0x78, 0xfc, 0xa2, 0x33, 0xc2, 0x3f, 0x52, 0xf8, 0x7e, 0x31, + 0x99, 0x4c, 0xae, 0xd5, 0x4d, 0x5f, 0x8c, 0x27, 0xac, 0xcb, 0x76, 0x7a, 0x18, 0xe5, 0x6d, 0x49, 0x71, 0xd8, 0x29, + 0x89, 0x76, 0xcb, 0xdb, 0x35, 0x46, 0xc9, 0x09, 0xea, 0xea, 0xf6, 0x4a, 0xac, 0x04, 0xaa, 0x2c, 0x41, 0x78, 0x9f, + 0xc4, 0x69, 0xd0, 0x2e, 0xfd, 0x53, 0x29, 0xf5, 0xbf, 0x78, 0xf4, 0xe8, 0x51, 0xe9, 0x8f, 0xd5, 0x5b, 0x7b, 0x3c, + 0x2e, 0xfd, 0xd1, 0x52, 0xa3, 0xd1, 0x6e, 0x4f, 0x26, 0xa5, 0x1f, 0xab, 0x82, 0xed, 0xee, 0x68, 0xbc, 0xdd, 0x2d, + 0xfd, 0x33, 0xa3, 0x45, 0xe9, 0x33, 0xf9, 0x96, 0xb3, 0x71, 0x2d, 0x54, 0xfc, 0xb0, 0x0d, 0x95, 0x82, 0xd1, 0x96, + 0xe8, 0xe0, 0x89, 0xc7, 0x20, 0x5a, 0xf0, 0x0c, 0x6c, 0xab, 0xb2, 0xc7, 0x40, 0x3e, 0x4f, 0xa4, 0x6c, 0x17, 0xdf, + 0x76, 0x45, 0x89, 0xfe, 0xab, 0x29, 0xd1, 0x91, 0x99, 0x49, 0x9a, 0x33, 0xd2, 0x03, 0xcd, 0x6a, 0xe4, 0x2c, 0xaa, + 0xfe, 0x35, 0x64, 0x95, 0xb0, 0x47, 0x69, 0x83, 0x2d, 0x85, 0x8c, 0xff, 0xf6, 0x2a, 0x19, 0xff, 0xdd, 0xcd, 0x32, + 0xfe, 0xf8, 0x76, 0x22, 0xfe, 0xbb, 0x3f, 0x58, 0xc4, 0x7f, 0x6b, 0x8a, 0x78, 0x21, 0xc4, 0x2e, 0xc0, 0x7a, 0x25, + 0xb3, 0xf5, 0x38, 0x3b, 0x6f, 0xe1, 0x96, 0xc8, 0x6d, 0x92, 0x9e, 0x1b, 0xb7, 0x12, 0xfe, 0x6b, 0x72, 0x7f, 0xd4, + 0x60, 0xc6, 0x87, 0x62, 0x79, 0x76, 0x72, 0x92, 0x30, 0x25, 0xe3, 0x8d, 0x0a, 0xb2, 0x88, 0xdf, 0xa4, 0xa1, 0xfd, + 0x06, 0x9c, 0x53, 0xa3, 0x64, 0x32, 0x81, 0xa2, 0xc9, 0xc4, 0x56, 0xb9, 0xb1, 0x20, 0xcf, 0xa8, 0xd5, 0xeb, 0x5a, + 0x09, 0xb5, 0xfa, 0xfa, 0x6b, 0xb3, 0xcc, 0x2c, 0x90, 0x51, 0x28, 0xd3, 0x98, 0x90, 0x35, 0xe3, 0xb8, 0xc0, 0x3d, + 0x58, 0x7d, 0xd8, 0x16, 0xed, 0x95, 0x19, 0x28, 0x95, 0x78, 0x84, 0x5f, 0x4c, 0x69, 0x7e, 0x44, 0x44, 0xe4, 0x31, + 0xaf, 0x22, 0x57, 0x9d, 0x75, 0x1a, 0xdf, 0xab, 0xab, 0xce, 0x37, 0x61, 0xf1, 0x65, 0x2e, 0xc3, 0xe3, 0x8b, 0x17, + 0x63, 0xe7, 0x02, 0xec, 0xd8, 0xb8, 0x78, 0x93, 0x36, 0x72, 0xc4, 0x04, 0xd8, 0x61, 0x68, 0x62, 0x5a, 0x0a, 0x82, + 0x55, 0xc9, 0xf2, 0x55, 0x65, 0xcf, 0xe8, 0x24, 0x53, 0x89, 0x70, 0xc8, 0x41, 0x8d, 0x2c, 0x81, 0x39, 0x98, 0xd4, + 0x85, 0xf4, 0xe1, 0x72, 0x91, 0x60, 0x71, 0x2a, 0xbf, 0x70, 0x4d, 0x91, 0xff, 0xa5, 0xd4, 0x1f, 0xf2, 0xe8, 0xbd, + 0xea, 0x89, 0xc1, 0x76, 0x31, 0xc3, 0xb8, 0x54, 0x01, 0x76, 0x20, 0xdc, 0x1c, 0x3f, 0xc7, 0x23, 0x86, 0x50, 0x71, + 0xec, 0x8a, 0x7a, 0xf8, 0xf9, 0x93, 0xea, 0xab, 0x90, 0xb5, 0x2f, 0x08, 0x36, 0x78, 0x00, 0xbf, 0xec, 0xcf, 0x51, + 0x1b, 0x64, 0x0b, 0xee, 0x38, 0xd4, 0xca, 0x71, 0x4b, 0xaf, 0xbb, 0xd3, 0x06, 0x15, 0xe3, 0x8b, 0x6f, 0xfe, 0x38, + 0xba, 0xb3, 0xc4, 0xf7, 0xaa, 0xb0, 0xf9, 0xca, 0x37, 0xb8, 0x34, 0x89, 0xf1, 0x23, 0x21, 0x02, 0x51, 0xe3, 0x9e, + 0x88, 0x5a, 0xc4, 0xe6, 0xbb, 0xaf, 0xdc, 0x37, 0x83, 0xb0, 0xee, 0x3a, 0x0e, 0x96, 0x31, 0xb2, 0x7a, 0x21, 0xb6, + 0x15, 0x56, 0xcd, 0x2a, 0x38, 0x37, 0xe8, 0xcc, 0xe2, 0xcc, 0x88, 0x39, 0xd7, 0xb6, 0x41, 0xa9, 0x82, 0xce, 0x22, + 0x72, 0x7c, 0x81, 0xf1, 0x51, 0xe1, 0xfb, 0x2a, 0xa0, 0xeb, 0x5e, 0xa7, 0x01, 0x39, 0xfa, 0xa3, 0x9a, 0xd1, 0x55, + 0x95, 0x2a, 0x28, 0xcd, 0x13, 0x02, 0x03, 0x19, 0x0a, 0xfe, 0xc2, 0x1a, 0xa7, 0x42, 0x6f, 0xc1, 0x34, 0x24, 0x80, + 0xb5, 0x47, 0x86, 0x6e, 0x89, 0xad, 0xc0, 0x16, 0xd2, 0x02, 0x94, 0x1e, 0x76, 0xe8, 0x5b, 0x35, 0xd0, 0xd3, 0xd5, + 0x98, 0xf1, 0x75, 0x4e, 0xda, 0xc5, 0x91, 0x5f, 0x9c, 0x79, 0xf0, 0xcf, 0xfa, 0x72, 0x09, 0x52, 0xfe, 0xf8, 0x53, + 0xcc, 0xc1, 0xa6, 0x9e, 0xb7, 0x30, 0x02, 0x42, 0x21, 0x4c, 0xa9, 0x0e, 0xe9, 0xd8, 0x51, 0x5c, 0x0f, 0xea, 0x2d, + 0x0a, 0xf4, 0xe5, 0xc8, 0x69, 0x09, 0xd2, 0x2c, 0x65, 0xbd, 0xfa, 0xf1, 0xb2, 0xe9, 0x37, 0x28, 0x62, 0x0d, 0x97, + 0x19, 0xfa, 0x7e, 0xfc, 0x02, 0x7c, 0x3f, 0xa1, 0x46, 0xdb, 0xca, 0x69, 0x68, 0xaf, 0x6d, 0x1f, 0x48, 0xda, 0x6e, + 0x92, 0xb5, 0x90, 0xaf, 0x3a, 0x47, 0x57, 0x39, 0x37, 0x37, 0x1d, 0xb6, 0x76, 0x77, 0x76, 0x3c, 0xf5, 0xcf, 0x38, + 0xa5, 0x6e, 0x16, 0xd3, 0x61, 0xeb, 0x6d, 0x20, 0x0b, 0xa2, 0x09, 0x7e, 0x4d, 0xef, 0x36, 0x2d, 0x8f, 0x29, 0xd3, + 0x71, 0x89, 0x6a, 0x3d, 0xe8, 0x3c, 0x02, 0x6f, 0xed, 0xd6, 0xc3, 0x5f, 0x8f, 0x7e, 0x29, 0x69, 0xa4, 0x2e, 0xac, + 0xda, 0x76, 0x0f, 0xe5, 0x45, 0x12, 0x5d, 0x80, 0xd3, 0x48, 0x36, 0xc6, 0x31, 0x06, 0x70, 0x7b, 0xf3, 0x4c, 0x66, + 0x0d, 0xe4, 0x2c, 0xa1, 0x5f, 0x59, 0x21, 0x97, 0x62, 0xfb, 0xc1, 0xfc, 0x5c, 0xad, 0x46, 0xa7, 0x91, 0x0d, 0xf0, + 0x87, 0x1e, 0xfa, 0x5f, 0x9d, 0x65, 0x50, 0x3f, 0xb8, 0xde, 0x01, 0x18, 0x84, 0x61, 0xd3, 0xca, 0x05, 0x54, 0x6d, + 0x28, 0x31, 0xd2, 0x1e, 0xaa, 0x81, 0x2c, 0x7f, 0x1b, 0x54, 0x65, 0x54, 0xb0, 0x1e, 0x7e, 0xd6, 0x30, 0x06, 0xd7, + 0x54, 0x1a, 0x4f, 0xb3, 0x78, 0x3c, 0x4e, 0x58, 0x4f, 0xd9, 0x47, 0x56, 0xe7, 0x01, 0x66, 0x0d, 0x98, 0x4b, 0x56, + 0x5f, 0x15, 0x83, 0x78, 0x9a, 0x4e, 0xd1, 0x31, 0xd8, 0x6b, 0xf8, 0x6d, 0xc2, 0xb5, 0xe4, 0x94, 0xc7, 0xe9, 0xed, + 0x8a, 0x78, 0xf4, 0x5c, 0xc7, 0x65, 0x07, 0x8c, 0x45, 0x5a, 0xf0, 0x76, 0x8f, 0x67, 0xf3, 0xa0, 0xb5, 0x5d, 0x47, + 0x04, 0xab, 0x34, 0x0a, 0xde, 0x1a, 0xb4, 0x3c, 0xb4, 0x0e, 0x84, 0x96, 0xb3, 0xfc, 0x8e, 0x2c, 0xa3, 0x01, 0xf0, + 0xfb, 0x77, 0xba, 0xa8, 0xac, 0x23, 0xf3, 0xff, 0x67, 0xb7, 0x7c, 0xb5, 0x7e, 0xb7, 0x7c, 0xa5, 0x76, 0xcb, 0xf5, + 0x1c, 0xfb, 0xc5, 0xa4, 0x83, 0x7f, 0x7a, 0x15, 0x42, 0xb0, 0x2a, 0x40, 0x0e, 0x0b, 0xed, 0xe2, 0x56, 0x17, 0xfe, + 0xa3, 0xa1, 0xdb, 0x1e, 0xfe, 0xf1, 0xc1, 0x02, 0x6c, 0x5b, 0x58, 0x88, 0xff, 0xda, 0xb5, 0xaa, 0xce, 0x7d, 0xac, + 0xc3, 0x5e, 0x3b, 0xab, 0x75, 0xdd, 0xeb, 0x37, 0x2d, 0xc8, 0x2b, 0xee, 0x04, 0x4a, 0x18, 0x83, 0xab, 0x16, 0x1d, + 0x1f, 0x43, 0xe9, 0x24, 0x1b, 0x2d, 0x8a, 0xbf, 0x97, 0xf0, 0x4b, 0x22, 0x5e, 0xbb, 0xa5, 0x1b, 0xe3, 0xa8, 0xae, + 0x22, 0x05, 0x45, 0x8d, 0xb0, 0xd4, 0xeb, 0x14, 0x14, 0xc0, 0x98, 0xcc, 0xe9, 0xfa, 0xf7, 0xd7, 0x6c, 0x82, 0xbf, + 0xc9, 0xda, 0xac, 0x45, 0xe6, 0xdf, 0x4b, 0x8c, 0x6b, 0x89, 0xf0, 0x59, 0x34, 0x30, 0xd7, 0xb0, 0xfd, 0x68, 0x3d, + 0xb8, 0x87, 0x6a, 0xa6, 0xa1, 0x52, 0x0a, 0x52, 0xef, 0x80, 0x17, 0x10, 0x2d, 0x12, 0x7e, 0xfd, 0xa8, 0x57, 0x71, + 0xc6, 0xca, 0xa8, 0xd7, 0x08, 0xf4, 0xaa, 0xed, 0x2d, 0xa5, 0xf4, 0x17, 0x5f, 0xdd, 0xc7, 0x3f, 0x22, 0xf0, 0x75, + 0x5c, 0xf9, 0x46, 0x22, 0x36, 0x80, 0xbe, 0xd1, 0x46, 0xcd, 0xf9, 0x11, 0x1a, 0x9c, 0xfc, 0x9f, 0xdb, 0xb6, 0x46, + 0x63, 0xfd, 0x56, 0xcd, 0xa5, 0x55, 0xfa, 0x59, 0xad, 0x3f, 0x6f, 0xf0, 0x5b, 0xb6, 0x1d, 0x09, 0x87, 0xa0, 0xde, + 0x56, 0xfe, 0xfa, 0x90, 0x95, 0xc6, 0x8a, 0xe2, 0xb7, 0x6d, 0x5f, 0x99, 0xc4, 0xd4, 0x63, 0x23, 0x3c, 0xd6, 0x4e, + 0xa4, 0x3c, 0x6f, 0xc6, 0x1e, 0xc2, 0x8f, 0xfc, 0x91, 0x85, 0xf7, 0xf0, 0xcb, 0x5b, 0xd6, 0xf9, 0x2c, 0x49, 0xc1, + 0xac, 0x9a, 0x72, 0x3e, 0x0f, 0xb6, 0xb6, 0xce, 0xce, 0xce, 0xfc, 0xb3, 0x6d, 0x3f, 0xcb, 0x4f, 0xb6, 0xba, 0xed, + 0x76, 0x1b, 0x3f, 0x98, 0x64, 0x5b, 0xa7, 0x31, 0x3b, 0x7b, 0x0c, 0xee, 0x87, 0xfd, 0xd0, 0x7a, 0x64, 0x3d, 0xdc, + 0xb6, 0x76, 0x1e, 0xd8, 0x16, 0x29, 0x00, 0x28, 0xd9, 0xb6, 0x2d, 0xa1, 0x00, 0x42, 0x1b, 0x8a, 0xfb, 0xbb, 0x27, + 0xca, 0x86, 0xc3, 0x84, 0x74, 0x61, 0x21, 0x81, 0xff, 0x96, 0x7d, 0x62, 0xf5, 0xad, 0x2e, 0xca, 0x5a, 0x52, 0x8d, + 0xa8, 0x57, 0xdc, 0xef, 0xa3, 0x68, 0x1e, 0x10, 0x1b, 0x99, 0x85, 0x18, 0x26, 0x13, 0xa5, 0x34, 0x05, 0xda, 0xa5, + 0xc7, 0xf0, 0x04, 0x6e, 0xc1, 0xd4, 0x82, 0xe7, 0x57, 0xdd, 0x87, 0xa0, 0xe3, 0x4e, 0x5b, 0xf7, 0x47, 0xed, 0x56, + 0xc7, 0xea, 0xb4, 0xba, 0xfe, 0x43, 0xab, 0x2b, 0xfe, 0x07, 0x19, 0xb9, 0x6d, 0x75, 0xe0, 0x69, 0xdb, 0x82, 0xf7, + 0xd3, 0xfb, 0x22, 0x1d, 0x22, 0xb2, 0xb7, 0xfa, 0xbb, 0xf8, 0xfb, 0x83, 0x00, 0xa9, 0xaf, 0x6c, 0xf1, 0x1b, 0xcf, + 0xec, 0x2f, 0xcc, 0xd2, 0xce, 0xa3, 0xb5, 0xc5, 0xdd, 0x87, 0x6b, 0x8b, 0xb7, 0x1f, 0xac, 0x2d, 0xbe, 0xbf, 0x53, + 0x2f, 0xde, 0x3a, 0x11, 0x55, 0x5a, 0x2e, 0x84, 0xf6, 0x2c, 0x02, 0x46, 0x39, 0x77, 0x3a, 0x00, 0x67, 0xdb, 0x6a, + 0xe1, 0x8f, 0x87, 0x5d, 0x57, 0xf7, 0x3a, 0xc6, 0x5e, 0x1a, 0xcb, 0x87, 0x8f, 0x00, 0xcb, 0xe7, 0xdd, 0x07, 0x23, + 0x6c, 0x47, 0x88, 0xc2, 0xbf, 0xd3, 0xed, 0x47, 0x23, 0xd0, 0x08, 0x16, 0xfe, 0x83, 0x3f, 0xd3, 0x9d, 0xee, 0x48, + 0xbc, 0xb4, 0xb1, 0xfe, 0x43, 0xe7, 0x61, 0x01, 0x4d, 0xf1, 0xcf, 0x6f, 0xda, 0x84, 0x46, 0x03, 0xde, 0x1c, 0xf7, + 0x3e, 0xd0, 0xe8, 0xd1, 0xb4, 0xeb, 0x7f, 0x75, 0xfa, 0xd0, 0x7f, 0x34, 0xed, 0x3c, 0xfc, 0x20, 0xde, 0x12, 0xa0, + 0xe0, 0x57, 0xf8, 0xef, 0xc3, 0x76, 0x7b, 0xda, 0xea, 0xf8, 0x8f, 0x4e, 0xb7, 0xfd, 0xed, 0xa4, 0xf5, 0xc0, 0x7f, + 0x84, 0xff, 0xaa, 0xe1, 0xa6, 0xd9, 0x8c, 0xd9, 0x16, 0xae, 0x77, 0xc3, 0xef, 0x35, 0xe7, 0xe8, 0xde, 0xb7, 0x76, + 0xee, 0x3f, 0x7f, 0x04, 0x6b, 0x34, 0xed, 0x74, 0xe1, 0xff, 0xab, 0x1e, 0x3f, 0x20, 0xe1, 0xe5, 0xc0, 0x11, 0xc3, + 0x54, 0x52, 0x45, 0x38, 0xfa, 0x78, 0xd7, 0x3d, 0xef, 0x87, 0xab, 0x02, 0x20, 0x7f, 0xbe, 0x39, 0x00, 0xf2, 0x97, + 0x5b, 0x06, 0xb9, 0xff, 0xfc, 0x07, 0x47, 0x40, 0x7e, 0x68, 0x06, 0xb9, 0xf7, 0xd8, 0x4a, 0xa0, 0xa3, 0xe9, 0xac, + 0x3d, 0x67, 0xce, 0xe1, 0x8f, 0x6c, 0x88, 0x69, 0xd3, 0xd0, 0xfa, 0x2f, 0xb5, 0x78, 0x50, 0x86, 0x1b, 0x79, 0x8f, + 0x89, 0x9d, 0xcc, 0xf8, 0x15, 0x04, 0xe1, 0xfc, 0x46, 0x82, 0xbc, 0xb8, 0x1d, 0x3d, 0x38, 0xff, 0x63, 0xe9, 0x41, + 0x5f, 0xee, 0x57, 0xf4, 0xa8, 0x45, 0xdc, 0x29, 0x62, 0x40, 0x8e, 0xfe, 0x3e, 0xbd, 0x3b, 0xf6, 0x16, 0xc3, 0xb7, + 0xc2, 0x16, 0xb9, 0x80, 0xef, 0x3e, 0xe7, 0x74, 0x40, 0x64, 0x15, 0x87, 0xb6, 0x0c, 0xc0, 0xcc, 0xf1, 0xdb, 0xb4, + 0xea, 0xe5, 0x54, 0xdc, 0x5c, 0x09, 0xe9, 0xda, 0xd9, 0x8e, 0x0e, 0xde, 0x60, 0xa2, 0x77, 0xb8, 0xcc, 0x78, 0x84, + 0xbf, 0xfc, 0x88, 0xc7, 0x3c, 0xc1, 0x4b, 0xb1, 0xf2, 0x02, 0x19, 0xe6, 0x25, 0x7f, 0x87, 0x39, 0xd5, 0xea, 0x90, + 0x60, 0x86, 0x01, 0x83, 0x57, 0x6c, 0x1c, 0x47, 0x8e, 0xed, 0xcc, 0x61, 0xc7, 0xc2, 0x98, 0xad, 0x5a, 0x42, 0x33, + 0xe5, 0x32, 0xbb, 0xb6, 0xfa, 0x7d, 0x3b, 0x39, 0x7e, 0xbf, 0x2c, 0x3c, 0x94, 0x01, 0x46, 0x5b, 0x7a, 0x00, 0x30, + 0xbe, 0x2a, 0xc9, 0x51, 0xd8, 0x57, 0x56, 0x83, 0x2d, 0xcc, 0x86, 0x8e, 0xdf, 0x05, 0x37, 0x82, 0x8a, 0xf1, 0x7b, + 0x50, 0x3f, 0x38, 0xad, 0x6d, 0x30, 0x6b, 0x8c, 0x6e, 0x7a, 0xa0, 0xe1, 0x4a, 0x18, 0x49, 0x04, 0x07, 0x1a, 0xa5, + 0x9e, 0xfe, 0x05, 0x64, 0x55, 0xb8, 0xa8, 0x78, 0x7c, 0x71, 0x20, 0xef, 0x7c, 0xdb, 0x18, 0xb9, 0xa5, 0x88, 0x7d, + 0xf5, 0xbd, 0xa9, 0x4d, 0x50, 0x17, 0xf4, 0x5b, 0x20, 0xe9, 0xdc, 0x1b, 0x35, 0x02, 0xa6, 0x5c, 0x5b, 0xd2, 0x73, + 0x08, 0x6d, 0xa1, 0x0f, 0xc6, 0xec, 0x34, 0x1e, 0x49, 0xb1, 0xee, 0x59, 0xf2, 0xaa, 0x48, 0x8b, 0xb0, 0x08, 0x3b, + 0x9e, 0xf0, 0x9d, 0xe1, 0x05, 0xb5, 0x5a, 0x98, 0x66, 0x76, 0xff, 0x5e, 0x4f, 0x43, 0x52, 0xcf, 0x56, 0xb7, 0xf1, + 0x57, 0x52, 0x1e, 0x82, 0xaf, 0xf6, 0xf7, 0xe1, 0x3d, 0xfc, 0xa5, 0x94, 0xf7, 0x86, 0xb6, 0xeb, 0x93, 0x50, 0xbc, + 0x57, 0xfd, 0x66, 0x4a, 0x94, 0x08, 0x9b, 0xa0, 0xbf, 0xbc, 0xdb, 0x2a, 0x32, 0xa9, 0xb4, 0xba, 0x3b, 0x95, 0xd2, + 0x82, 0x67, 0x43, 0x4a, 0x81, 0x00, 0xed, 0xfa, 0x3b, 0x86, 0x28, 0x3c, 0x6d, 0xe1, 0xcf, 0x9a, 0x30, 0xbc, 0x0f, + 0x0d, 0x94, 0x34, 0x7c, 0x09, 0xcd, 0xb7, 0x85, 0xe0, 0x85, 0x7e, 0x3f, 0x92, 0xa8, 0x12, 0x62, 0xaa, 0xce, 0x31, + 0x6b, 0x0e, 0x91, 0x44, 0x8e, 0x80, 0xed, 0x19, 0xf1, 0x26, 0xc1, 0xae, 0x32, 0x9a, 0xf2, 0x14, 0xfa, 0x3a, 0xfa, + 0x33, 0xce, 0xeb, 0xea, 0xbc, 0xda, 0xce, 0x59, 0x33, 0x05, 0x32, 0x7c, 0xe3, 0xa0, 0x8a, 0xae, 0x2e, 0x88, 0xcf, + 0x99, 0x89, 0x6d, 0x5c, 0x7d, 0xf0, 0x6d, 0x4d, 0xf6, 0xad, 0xb9, 0x29, 0x58, 0xc5, 0x34, 0xb4, 0x2f, 0x30, 0x65, + 0x06, 0x7f, 0x56, 0xc5, 0xea, 0x41, 0x32, 0x94, 0x9f, 0x44, 0xf8, 0xdb, 0x58, 0xe8, 0x47, 0x59, 0x6d, 0x40, 0x4e, + 0xdf, 0xab, 0x24, 0x48, 0x5f, 0x8c, 0xcb, 0x26, 0x12, 0x60, 0x2f, 0xe0, 0x2f, 0xf7, 0xab, 0xae, 0x4a, 0xc8, 0x3b, + 0x90, 0x98, 0x53, 0x30, 0x8e, 0x73, 0xba, 0x5a, 0xab, 0xf0, 0xaf, 0x45, 0x34, 0x2b, 0x52, 0xd3, 0xae, 0x64, 0xc5, + 0xc0, 0xc6, 0x22, 0x3b, 0x90, 0xc9, 0x68, 0xe6, 0x07, 0x9b, 0xcd, 0xbb, 0x8f, 0x63, 0x91, 0x87, 0x86, 0x1f, 0xb4, + 0xb7, 0x05, 0x91, 0x6d, 0x10, 0x63, 0x57, 0xe2, 0x44, 0xc6, 0x0d, 0x5e, 0x19, 0xac, 0x7e, 0x43, 0x91, 0xb9, 0xe1, + 0x6d, 0x73, 0xb5, 0xf4, 0xb8, 0xb4, 0x0e, 0xae, 0x8c, 0xdf, 0x1d, 0xb3, 0x88, 0xfb, 0x51, 0x4a, 0xb9, 0x49, 0x8e, + 0x21, 0x16, 0xbc, 0x0e, 0xdb, 0x76, 0x4b, 0x90, 0x3c, 0xc6, 0xaf, 0x70, 0x12, 0xa4, 0xf7, 0xa1, 0xb0, 0x4a, 0xd8, + 0xda, 0x9d, 0x76, 0xfb, 0x6f, 0x0e, 0xf6, 0x2c, 0xb1, 0x9b, 0x77, 0xb7, 0xe0, 0x75, 0x97, 0xdc, 0x61, 0x91, 0x9f, + 0x11, 0x8a, 0xfc, 0x0c, 0x4b, 0x24, 0x74, 0x85, 0xf6, 0x96, 0x40, 0xd3, 0xb6, 0x58, 0x3a, 0x12, 0x31, 0xbc, 0x19, + 0xb8, 0x0b, 0x31, 0x7e, 0xd4, 0x6b, 0x0b, 0xbb, 0xb5, 0x70, 0xa5, 0x6d, 0x95, 0xe1, 0xa2, 0x0c, 0x04, 0x9e, 0xaa, + 0x88, 0x1f, 0xa8, 0x75, 0xa6, 0x92, 0x5d, 0xe4, 0x50, 0x3a, 0x27, 0x75, 0xb5, 0x75, 0xb1, 0x38, 0x9e, 0x81, 0x1c, + 0x52, 0x09, 0x2a, 0xef, 0x65, 0x87, 0x5d, 0x9a, 0x0a, 0x93, 0x62, 0x57, 0x23, 0x92, 0xd3, 0x4e, 0x7f, 0x37, 0x92, + 0xf6, 0x0e, 0xee, 0xdd, 0x02, 0x36, 0x2f, 0xa8, 0x39, 0x34, 0x2a, 0xfc, 0x38, 0xdb, 0x3a, 0x63, 0xc7, 0xad, 0x68, + 0x1e, 0x57, 0xe1, 0x3f, 0xd4, 0x7e, 0xfd, 0x5d, 0xa5, 0x08, 0x65, 0x9a, 0xa5, 0x7c, 0x8c, 0x8c, 0x2c, 0x0e, 0x24, + 0x1c, 0x31, 0x68, 0x29, 0x63, 0x8b, 0x64, 0x34, 0x02, 0xf1, 0x01, 0x56, 0xe2, 0x5f, 0x15, 0x83, 0x94, 0x9a, 0xa0, + 0xb4, 0xfb, 0x7f, 0xfd, 0x5f, 0xff, 0x5b, 0x86, 0x15, 0x81, 0xac, 0x00, 0x16, 0xa6, 0xc1, 0x54, 0x27, 0x8c, 0xec, + 0x1c, 0x1c, 0xd1, 0x78, 0xdc, 0x9a, 0x46, 0xc9, 0x04, 0x20, 0x28, 0x98, 0xb8, 0xca, 0x24, 0xeb, 0x81, 0x0b, 0x24, + 0x58, 0xe6, 0xe1, 0xbc, 0x04, 0xaf, 0x5e, 0x84, 0x2b, 0xf6, 0xbb, 0xf2, 0x56, 0x55, 0xbe, 0x30, 0x31, 0xb4, 0x91, + 0xc5, 0x6a, 0xf0, 0x5c, 0x2d, 0x93, 0x55, 0xfd, 0x82, 0x24, 0x29, 0x3c, 0x58, 0x2d, 0x8d, 0x15, 0x5a, 0xea, 0x83, + 0x90, 0x7f, 0xfb, 0xe7, 0xff, 0xfc, 0xdf, 0xd5, 0x2b, 0x9e, 0x6f, 0xfc, 0xf5, 0x9f, 0xfe, 0xe1, 0xff, 0xfe, 0x9f, + 0xff, 0x82, 0x59, 0xc2, 0xf2, 0x0c, 0x84, 0xb6, 0x92, 0x55, 0x1d, 0x80, 0x88, 0x3d, 0x65, 0x55, 0x0e, 0x47, 0x3d, + 0xdd, 0x75, 0x9f, 0x26, 0x24, 0xde, 0x94, 0xd0, 0x11, 0x5f, 0x53, 0x7a, 0x34, 0x51, 0xed, 0x1a, 0xf2, 0xc1, 0x52, + 0x5a, 0x74, 0xac, 0x6f, 0xef, 0xb4, 0xed, 0x6a, 0x79, 0xfb, 0x46, 0xdf, 0x2d, 0x5c, 0x98, 0x5b, 0x65, 0xe0, 0xf8, + 0x7a, 0xd9, 0x96, 0x2a, 0x8c, 0x85, 0x25, 0x65, 0x55, 0x6e, 0x61, 0x7c, 0x79, 0x89, 0xaf, 0x41, 0xd7, 0x28, 0xa6, + 0x55, 0xae, 0xf5, 0xe9, 0xfd, 0xb2, 0x00, 0x44, 0x27, 0xb8, 0x34, 0x22, 0x58, 0x46, 0x67, 0xa7, 0x2d, 0xb4, 0x4e, + 0x92, 0x8b, 0x92, 0x46, 0x11, 0xde, 0xcc, 0xfd, 0x47, 0x7f, 0x57, 0xfe, 0x69, 0x86, 0x56, 0x81, 0xe5, 0xcc, 0xa2, + 0x73, 0xe9, 0xe3, 0x3c, 0x68, 0xb7, 0xe7, 0xe7, 0xee, 0xb2, 0x9a, 0xc1, 0xbb, 0x6a, 0x32, 0x0a, 0xb0, 0x99, 0x03, + 0xd2, 0xa1, 0xab, 0x8e, 0xe5, 0x81, 0x59, 0xdf, 0xc6, 0xd0, 0x4f, 0x59, 0x7e, 0xb9, 0xa4, 0x70, 0x52, 0xfc, 0x1b, + 0x1e, 0x8e, 0xca, 0xc8, 0x1b, 0x94, 0x18, 0x58, 0x2c, 0x8d, 0x5e, 0x5d, 0xd1, 0x6b, 0xda, 0x59, 0xcd, 0x4d, 0x31, + 0x0f, 0x77, 0xcd, 0x63, 0xd9, 0xfb, 0x78, 0xd0, 0x3a, 0xed, 0x78, 0xd3, 0xee, 0x52, 0x0f, 0xcf, 0x79, 0x36, 0x33, + 0x4f, 0x73, 0x59, 0xc4, 0x46, 0x6c, 0xa2, 0x22, 0x96, 0xb2, 0x5e, 0x9c, 0xd4, 0x96, 0x5f, 0xe0, 0x76, 0x03, 0xda, + 0x66, 0x11, 0x0f, 0x88, 0x69, 0x7b, 0xe6, 0x79, 0x6f, 0x84, 0x27, 0xe9, 0xd9, 0xd2, 0x98, 0xab, 0x27, 0x9a, 0x62, + 0x5c, 0xb0, 0x9e, 0xf7, 0x53, 0xfa, 0xd4, 0xdd, 0x1c, 0x4a, 0x84, 0x15, 0x5e, 0xc8, 0x63, 0xd4, 0x77, 0x35, 0x7f, + 0x5c, 0x8a, 0x62, 0x70, 0x81, 0xd7, 0xd6, 0x0b, 0xb5, 0x28, 0x6a, 0x5f, 0x80, 0xb5, 0x43, 0x60, 0xda, 0xcd, 0x56, + 0x54, 0x88, 0xad, 0xde, 0x85, 0x2f, 0xb4, 0xed, 0x1d, 0xcd, 0xe7, 0xd4, 0xd0, 0x05, 0x6e, 0x24, 0x1b, 0x1a, 0x25, + 0x05, 0xa5, 0x08, 0x88, 0x13, 0x79, 0xd9, 0x46, 0xb2, 0xad, 0x78, 0x92, 0x67, 0xf5, 0xf4, 0xfb, 0xb6, 0xff, 0x1f, + 0x22, 0x28, 0x4d, 0x5d, 0x85, 0x7b, 0x00, 0x00}; } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f0a7efd12f..30ac959e43 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -360,9 +360,14 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { - std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - if (!obj->get_unit_of_measurement().empty()) - state += " " + obj->get_unit_of_measurement(); + std::string state; + if (isnan(value)) { + state = "NA"; + } else { + state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); + if (!obj->get_unit_of_measurement().empty()) + state += " " + obj->get_unit_of_measurement(); + } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); }); } @@ -719,12 +724,15 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["step"] = obj->traits.get_step(); root["mode"] = (int) obj->traits.get_mode(); } - std::string state = str_sprintf("%f", value); - root["state"] = state; if (isnan(value)) { root["value"] = "\"NaN\""; + root["state"] = "NA"; } else { root["value"] = value; + std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); + if (!obj->traits.get_unit_of_measurement().empty()) + state += " " + obj->traits.get_unit_of_measurement(); + root["state"] = state; } }); } @@ -839,6 +847,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf return json::build_json([obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); + int8_t accuracy = traits.get_temperature_accuracy_decimals(); char __buf[16]; if (start_config == DETAIL_ALL) { @@ -873,12 +882,15 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf } } + bool has_state = false; root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root["max_temp"] = traits.get_visual_max_temperature(); - root["min_temp"] = traits.get_visual_min_temperature(); + root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), accuracy); + root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), accuracy); root["step"] = traits.get_visual_temperature_step(); if (traits.get_supports_action()) { root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); + root["state"] = root["action"]; + has_state = true; } if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); @@ -896,14 +908,23 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); } if (traits.get_supports_current_temperature()) { - root["current_temperature"] = obj->current_temperature; + if (!std::isnan(obj->current_temperature)) { + root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, accuracy); + } else { + root["current_temperature"] = "NA"; + } } if (traits.get_supports_two_point_target_temperature()) { - root["current_temperature_low"] = obj->target_temperature_low; - root["current_temperature_high"] = obj->target_temperature_low; + root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, accuracy); + root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, accuracy); + if (!has_state) { + root["state"] = + value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, accuracy); + } } else { - root["target_temperature"] = obj->target_temperature; - root["state"] = obj->target_temperature; + root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, accuracy); + if (!has_state) + root["state"] = root["target_temperature"]; } }); } diff --git a/esphome/components/whynter/__init__.py b/esphome/components/whynter/__init__.py new file mode 100644 index 0000000000..7316776d78 --- /dev/null +++ b/esphome/components/whynter/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@aeonsablaze"] diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py new file mode 100644 index 0000000000..b9dc5868bc --- /dev/null +++ b/esphome/components/whynter/climate.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +whynter_ns = cg.esphome_ns.namespace("whynter") +Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Whynter), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp new file mode 100644 index 0000000000..190bf70acc --- /dev/null +++ b/esphome/components/whynter/whynter.cpp @@ -0,0 +1,181 @@ +#include "whynter.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace whynter { + +static const char *const TAG = "climate.whynter"; + +const uint16_t BITS = 32; + +// Static First Byte +const uint32_t COMMAND_MASK = 0xFF << 24; +const uint32_t COMMAND_CODE = 0x12 << 24; + +// Power +const uint32_t POWER_SHIFT = 8; +const uint32_t POWER_MASK = 1 << POWER_SHIFT; +const uint32_t POWER_OFF = 0 << POWER_SHIFT; + +// Mode +const uint32_t MODE_SHIFT = 16; +const uint32_t MODE_MASK = 0b1111 << MODE_SHIFT; +const uint32_t MODE_FAN = 0b0001 << MODE_SHIFT; +const uint32_t MODE_DRY = 0b0010 << MODE_SHIFT; +const uint32_t MODE_HEAT = 0b0100 << MODE_SHIFT; +const uint32_t MODE_COOL = 0b1000 << MODE_SHIFT; + +// Fan Speed +const uint32_t FAN_SHIFT = 20; +const uint32_t FAN_MASK = 0b111 << FAN_SHIFT; +const uint32_t FAN_HIGH = 0b001 << FAN_SHIFT; +const uint32_t FAN_MED = 0b010 << FAN_SHIFT; +const uint32_t FAN_LOW = 0b100 << FAN_SHIFT; + +// Temperature Unit +const uint32_t UNIT_SHIFT = 10; +const uint32_t UNIT_MASK = 1 << UNIT_SHIFT; + +// Temperature Value +const uint32_t TEMP_MASK = 0xFF; +const uint32_t TEMP_OFFSET_C = 16; + +void Whynter::transmit_state() { + uint32_t remote_state = COMMAND_CODE; + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) + this->mode = climate::CLIMATE_MODE_COOL; + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state |= POWER_MASK; + remote_state |= MODE_FAN; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= POWER_MASK; + remote_state |= MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= POWER_MASK; + remote_state |= MODE_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + remote_state |= POWER_MASK; + remote_state |= MODE_COOL; + break; + case climate::CLIMATE_MODE_OFF: + remote_state |= POWER_OFF; + break; + default: + remote_state |= POWER_OFF; + } + mode_before_ = this->mode; + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_LOW; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_HIGH; + break; + default: + remote_state |= FAN_HIGH; + } + + if (fahrenheit_) { + remote_state |= UNIT_MASK; + uint8_t temp = + (uint8_t) clamp(esphome::celsius_to_fahrenheit(this->target_temperature), TEMP_MIN_F, TEMP_MAX_F); + temp = esphome::reverse_bits(temp); + remote_state |= temp; + } else { + uint8_t temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN_C, TEMP_MAX_C) - TEMP_OFFSET_C); + temp = esphome::reverse_bits(temp); + remote_state |= temp; + } + + transmit_(remote_state); + this->publish_state(); +} + +bool Whynter::on_receive(remote_base::RemoteReceiveData data) { + uint8_t nbits = 0; + uint32_t remote_state = 0; + + if (!data.expect_item(this->header_high_, this->header_low_)) + return false; + + for (nbits = 0; nbits < 32; nbits++) { + if (data.expect_item(this->bit_high_, this->bit_one_low_)) { + remote_state = (remote_state << 1) | 1; + } else if (data.expect_item(this->bit_high_, this->bit_zero_low_)) { + remote_state = (remote_state << 1) | 0; + } else if (nbits == BITS) { + break; + } else { + return false; + } + } + + ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + if ((remote_state & COMMAND_MASK) != COMMAND_CODE) + return false; + if ((remote_state & POWER_MASK) != POWER_MASK) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + if ((remote_state & MODE_MASK) == MODE_FAN) { + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + } else if ((remote_state & MODE_MASK) == MODE_DRY) { + this->mode = climate::CLIMATE_MODE_DRY; + } else if ((remote_state & MODE_MASK) == MODE_HEAT) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else if ((remote_state & MODE_MASK) == MODE_COOL) { + this->mode = climate::CLIMATE_MODE_COOL; + } + + // Temperature + if ((remote_state & UNIT_MASK) == UNIT_MASK) { // Fahrenheit + this->target_temperature = esphome::fahrenheit_to_celsius(esphome::reverse_bits(remote_state & TEMP_MASK) >> 24); + } else { // Celsius + this->target_temperature = (esphome::reverse_bits(remote_state & TEMP_MASK) >> 24) + TEMP_OFFSET_C; + } + + // Fan Speed + if ((remote_state & FAN_MASK) == FAN_LOW) { + this->fan_mode = climate::CLIMATE_FAN_LOW; + } else if ((remote_state & FAN_MASK) == FAN_MED) { + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + } else if ((remote_state & FAN_MASK) == FAN_HIGH) { + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + } + this->publish_state(); + + return true; +} + +void Whynter::transmit_(uint32_t value) { + ESP_LOGD(TAG, "Sending whynter code: 0x%02X", value); + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + + data->set_carrier_frequency(38000); + data->reserve(2 + BITS * 2u); + + data->item(this->header_high_, this->header_low_); + + for (uint32_t mask = 1UL << (BITS - 1); mask != 0; mask >>= 1) { + if (value & mask) { + data->item(this->bit_high_, this->bit_one_low_); + } else { + data->item(this->bit_high_, this->bit_zero_low_); + } + } + data->mark(this->bit_high_); + transmit.perform(); +} + +} // namespace whynter +} // namespace esphome diff --git a/esphome/components/whynter/whynter.h b/esphome/components/whynter/whynter.h new file mode 100644 index 0000000000..939583ebfb --- /dev/null +++ b/esphome/components/whynter/whynter.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace whynter { + +// Temperature +const uint8_t TEMP_MIN_C = 16; // Celsius +const uint8_t TEMP_MAX_C = 32; // Celsius +const uint8_t TEMP_MIN_F = 61; // Fahrenheit +const uint8_t TEMP_MAX_F = 89; // Fahrenheit + +class Whynter : public climate_ir::ClimateIR { + public: + Whynter() + : climate_ir::ClimateIR(TEMP_MIN_C, TEMP_MAX_C, 1.0, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, {}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { climate_ir::ClimateIR::control(call); } + + // Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = 1.0f; + this->minimum_temperature_ = esphome::fahrenheit_to_celsius(TEMP_MIN_F); + this->maximum_temperature_ = esphome::fahrenheit_to_celsius(TEMP_MAX_F); + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + void transmit_(uint32_t value); + + uint32_t header_high_ = 8000; + uint32_t header_low_ = 4000; + uint32_t bit_high_ = 600; + uint32_t bit_one_low_ = 1600; + uint32_t bit_zero_low_ = 550; + + bool fahrenheit_{false}; + + climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; +}; + +} // namespace whynter +} // namespace esphome diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 615a6905bc..7941e7f643 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -182,6 +182,7 @@ class WiFiComponent : public Component { * can be made, the AP will be turned off again. */ void set_ap(const WiFiAP &ap); + WiFiAP get_ap() { return this->ap_; } void start_scanning(); void check_scanning_finished(); diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 83381f3424..e893712287 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -195,7 +195,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { #if ESP_IDF_VERSION_MAJOR >= 4 // Protected Management Frame - // Device will prefer to connect in PMF mode if other device also advertizes PMF capability. + // Device will prefer to connect in PMF mode if other device also advertises PMF capability. conf.sta.pmf_cfg.capable = true; conf.sta.pmf_cfg.required = false; #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index b838e42b0d..6d20219c69 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -303,7 +303,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { #if ESP_IDF_VERSION_MAJOR >= 4 // Protected Management Frame - // Device will prefer to connect in PMF mode if other device also advertizes PMF capability. + // Device will prefer to connect in PMF mode if other device also advertises PMF capability. conf.sta.pmf_cfg.capable = true; conf.sta.pmf_cfg.required = false; #endif diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index 4ed5f587de..29c9de1652 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -38,7 +38,7 @@ bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (this->impedance_ != nullptr) { if (res->version == 1) { - ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as verison 1."); + ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as version 1."); } else { if (res->impedance.has_value()) { this->impedance_->publish_state(*res->impedance); diff --git a/esphome/config.py b/esphome/config.py index a878f3ef79..545b805367 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -679,6 +679,7 @@ def validate_config(config, command_line_substitutions) -> Config: result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: substitutions.do_substitution_pass(config, command_line_substitutions) + substitutions.do_substitution_pass(config, command_line_substitutions) except vol.Invalid as err: result.add_error(err) return result diff --git a/esphome/config_validation.py b/esphome/config_validation.py index dacbcf72ba..0ff0ba83d9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -57,11 +57,12 @@ from esphome.core import ( TimePeriodMinutes, ) from esphome.helpers import list_starts_with, add_class_to_obj -from esphome.jsonschema import ( - jschema_list, - jschema_extractor, - jschema_registry, - jschema_typed, +from esphome.schema_extractors import ( + SCHEMA_EXTRACT, + schema_extractor_list, + schema_extractor, + schema_extractor_registry, + schema_extractor_typed, ) from esphome.util import parse_esphome_version from esphome.voluptuous_schema import _Schema @@ -327,7 +328,7 @@ def boolean(value): ) -@jschema_list +@schema_extractor_list def ensure_list(*validators): """Validate this configuration option to be a list. @@ -452,7 +453,11 @@ def validate_id_name(value): def use_id(type): """Declare that this configuration option should point to an ID with the given type.""" + @schema_extractor("use_id") def validator(value): + if value == SCHEMA_EXTRACT: + return type + check_not_templatable(value) if value is None: return core.ID(None, is_declaration=False, type=type) @@ -475,7 +480,11 @@ def declare_id(type): If two IDs with the same name exist, a validation error is thrown. """ + @schema_extractor("declare_id") def validator(value): + if value == SCHEMA_EXTRACT: + return type + check_not_templatable(value) if value is None: return core.ID(None, is_declaration=True, type=type) @@ -494,11 +503,11 @@ def templatable(other_validators): """ schema = Schema(other_validators) - @jschema_extractor("templatable") + @schema_extractor("templatable") def validator(value): - # pylint: disable=comparison-with-callable - if value == jschema_extractor: + if value == SCHEMA_EXTRACT: return other_validators + if isinstance(value, Lambda): return returning_lambda(value) if isinstance(other_validators, dict): @@ -963,9 +972,9 @@ def ipv4(value): elif isinstance(value, IPAddress): return value else: - raise Invalid("IPv4 address must consist of either string or " "integer list") + raise Invalid("IPv4 address must consist of either string or integer list") if len(parts) != 4: - raise Invalid("IPv4 address must consist of four point-separated " "integers") + raise Invalid("IPv4 address must consist of four point-separated integers") parts_ = list(map(int, parts)) if not all(0 <= x < 256 for x in parts_): raise Invalid("IPv4 address parts must be in range from 0 to 255") @@ -985,10 +994,10 @@ def _valid_topic(value): raise Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: raise Invalid( - "MQTT topic name/filter must not be longer than " "65535 encoded bytes." + "MQTT topic name/filter must not be longer than 65535 encoded bytes." ) if "\0" in value: - raise Invalid("MQTT topic name/filter must not contain null " "character.") + raise Invalid("MQTT topic name/filter must not contain null character.") return value @@ -1000,7 +1009,7 @@ def subscribe_topic(value): i < len(value) - 1 and value[i + 1] != "/" ): raise Invalid( - "Single-level wildcard must occupy an entire " "level of the filter" + "Single-level wildcard must occupy an entire level of the filter" ) index = value.find("#") @@ -1012,9 +1021,7 @@ def subscribe_topic(value): "character in the topic filter." ) if len(value) > 1 and value[index - 1] != "/": - raise Invalid( - "Multi-level wildcard must be after a topic " "level separator." - ) + raise Invalid("Multi-level wildcard must be after a topic level separator.") return value @@ -1177,10 +1184,9 @@ def one_of(*values, **kwargs): if kwargs: raise ValueError - @jschema_extractor("one_of") + @schema_extractor("one_of") def validator(value): - # pylint: disable=comparison-with-callable - if value == jschema_extractor: + if value == SCHEMA_EXTRACT: return values if string_: @@ -1220,10 +1226,9 @@ def enum(mapping, **kwargs): assert isinstance(mapping, dict) one_of_validator = one_of(*mapping, **kwargs) - @jschema_extractor("enum") + @schema_extractor("enum") def validator(value): - # pylint: disable=comparison-with-callable - if value == jschema_extractor: + if value == SCHEMA_EXTRACT: return mapping value = one_of_validator(value) @@ -1396,7 +1401,7 @@ def extract_keys(schema): return keys -@jschema_typed +@schema_extractor_typed def typed_schema(schemas, **kwargs): """Create a schema that has a key to distinguish between schemas""" key = kwargs.pop("key", CONF_TYPE) @@ -1510,7 +1515,7 @@ def validate_registry_entry(name, registry): ) ignore_keys = extract_keys(base_schema) - @jschema_registry(registry) + @schema_extractor_registry(registry) def validator(value): if isinstance(value, str): value = {value: {}} @@ -1555,12 +1560,15 @@ def validate_registry(name, registry): return ensure_list(validate_registry_entry(name, registry)) -@jschema_list def maybe_simple_value(*validators, **kwargs): key = kwargs.pop("key", CONF_VALUE) validator = All(*validators) + @schema_extractor("maybe") def validate(value): + if value == SCHEMA_EXTRACT: + return (validator, key) + if isinstance(value, dict) and key in value: return validator(value) return validator({key: value}) diff --git a/esphome/const.py b/esphome/const.py index f95628735d..981ee773d4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.6.3" +__version__ = "2022.8.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -88,6 +88,7 @@ CONF_CERTIFICATE_AUTHORITY = "certificate_authority" CONF_CHANGE_MODE_EVERY = "change_mode_every" CONF_CHANNEL = "channel" CONF_CHANNELS = "channels" +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_CHIPSET = "chipset" CONF_CLEAR_IMPEDANCE = "clear_impedance" CONF_CLIENT_ID = "client_id" @@ -126,6 +127,7 @@ CONF_COOL_MODE = "cool_mode" CONF_COOL_OVERRUN = "cool_overrun" CONF_COUNT = "count" CONF_COUNT_MODE = "count_mode" +CONF_COUNTS = "counts" CONF_COURSE = "course" CONF_CRON = "cron" CONF_CS_PIN = "cs_pin" @@ -164,6 +166,7 @@ CONF_DELTA = "delta" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" +CONF_DIELECTRIC_CONSTANT = "dielectric_constant" CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" @@ -533,6 +536,7 @@ CONF_PRESSURE = "pressure" CONF_PRIORITY = "priority" CONF_PROJECT = "project" CONF_PROTOCOL = "protocol" +CONF_PUBLISH_INITIAL_STATE = "publish_initial_state" CONF_PULL_MODE = "pull_mode" CONF_PULLDOWN = "pulldown" CONF_PULLUP = "pullup" @@ -749,6 +753,7 @@ CONF_WAND_ID = "wand_id" CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" +CONF_WEB_SERVER = "web_server" CONF_WEIGHT = "weight" CONF_WHILE = "while" CONF_WHITE = "white" @@ -905,12 +910,15 @@ DEVICE_CLASS_BATTERY = "battery" DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_POWER = "power" # device classes of sensor component +DEVICE_CLASS_APPARENT_POWER = "apparent_power" DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_DATE = "date" DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_ENERGY = "energy" +DEVICE_CLASS_FREQUENCY = "frequency" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" DEVICE_CLASS_MONETARY = "monetary" @@ -923,6 +931,7 @@ DEVICE_CLASS_PM10 = "pm10" DEVICE_CLASS_PM25 = "pm25" DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_REACTIVE_POWER = "reactive_power" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" DEVICE_CLASS_TEMPERATURE = "temperature" @@ -947,6 +956,9 @@ STATE_CLASS_MEASUREMENT = "measurement" # The state represents a total that only increases, a decrease is considered a reset. STATE_CLASS_TOTAL_INCREASING = "total_increasing" +# The state represents a total amount that can both increase and decrease, e.g. a net energy meter. +STATE_CLASS_TOTAL = "total" + KEY_CORE = "core" KEY_TARGET_PLATFORM = "target_platform" KEY_TARGET_FRAMEWORK = "target_framework" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a4867915bb..3ee94efd64 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_ESPHOME, CONF_USE_ADDRESS, CONF_ETHERNET, + CONF_WEB_SERVER, CONF_WIFI, CONF_PORT, KEY_CORE, @@ -512,7 +513,7 @@ class EsphomeCore: if self.config is None: raise ValueError("Config has not been loaded yet") - if "wifi" in self.config: + if CONF_WIFI in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] if CONF_ETHERNET in self.config: @@ -525,9 +526,9 @@ class EsphomeCore: if self.config is None: raise ValueError("Config has not been loaded yet") - if "web_server" in self.config: + if CONF_WEB_SERVER in self.config: try: - return self.config["web_server"][CONF_PORT] + return self.config[CONF_WEB_SERVER][CONF_PORT] except KeyError: return 80 @@ -651,7 +652,7 @@ class EsphomeCore: continue if other.repository is not None: if library.repository is None or other.repository == library.repository: - # Other is using a/the same repository, takes precendence + # Other is using a/the same repository, takes precedence break raise ValueError( f"Adding named Library with repository failed! Libraries {library} and {other} " diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index a423397453..c012195f34 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -125,19 +125,26 @@ void IRAM_ATTR HOT Application::feed_wdt() { } void Application::reboot() { ESP_LOGI(TAG, "Forcing a reboot..."); - for (auto *comp : this->components_) - comp->on_shutdown(); + for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { + (*it)->on_shutdown(); + } arch_restart(); } void Application::safe_reboot() { ESP_LOGI(TAG, "Rebooting safely..."); - for (auto *comp : this->components_) - comp->on_safe_shutdown(); - for (auto *comp : this->components_) - comp->on_shutdown(); + run_safe_shutdown_hooks(); arch_restart(); } +void Application::run_safe_shutdown_hooks() { + for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { + (*it)->on_safe_shutdown(); + } + for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { + (*it)->on_shutdown(); + } +} + void Application::calculate_looping_components_() { for (auto *obj : this->components_) { if (obj->has_overridden_loop()) diff --git a/esphome/core/application.h b/esphome/core/application.h index 453b15822e..6376987f66 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -161,14 +161,7 @@ class Application { void safe_reboot(); - void run_safe_shutdown_hooks() { - for (auto *comp : this->components_) { - comp->on_safe_shutdown(); - } - for (auto *comp : this->components_) { - comp->on_shutdown(); - } - } + void run_safe_shutdown_hooks(); uint32_t get_app_state() const { return this->app_state_; } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index e87a4a2765..6ac6e596c1 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -93,7 +93,12 @@ class StartupTrigger : public Trigger<>, public Component { class ShutdownTrigger : public Trigger<>, public Component { public: + explicit ShutdownTrigger(float setup_priority) : setup_priority_(setup_priority) {} void on_shutdown() override { this->trigger(); } + float get_setup_priority() const override { return this->setup_priority_; } + + protected: + float setup_priority_; }; class LoopTrigger : public Trigger<>, public Component { diff --git a/esphome/core/color.h b/esphome/core/color.h index c9ca3bcfc3..7596eeb0cf 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -139,8 +139,19 @@ struct Color { return Color(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)), uint8_t((uint16_t(b) * 255U / max_rgb)), w); } - Color fade_to_white(uint8_t amnt) { return Color(255, 255, 255, 255) - (*this * amnt); } - Color fade_to_black(uint8_t amnt) { return *this * amnt; } + + Color gradient(const Color &to_color, uint8_t amnt) { + Color new_color; + float amnt_f = float(amnt) / 255.0f; + new_color.r = amnt_f * (to_color.r - (*this).r) + (*this).r; + new_color.g = amnt_f * (to_color.g - (*this).g) + (*this).g; + new_color.b = amnt_f * (to_color.b - (*this).b) + (*this).b; + new_color.w = amnt_f * (to_color.w - (*this).w) + (*this).w; + return new_color; + } + Color fade_to_white(uint8_t amnt) { return (*this).gradient(Color::WHITE, amnt); } + Color fade_to_black(uint8_t amnt) { return (*this).gradient(Color::BLACK, amnt); } + Color lighten(uint8_t delta) { return *this + delta; } Color darken(uint8_t delta) { return *this - delta; } diff --git a/esphome/core/config.py b/esphome/core/config.py index 68c253f7b4..f1337be04b 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -117,6 +117,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ShutdownTrigger), + cv.Optional(CONF_PRIORITY, default=600.0): cv.float_, } ), cv.Optional(CONF_ON_LOOP): automation.validate_automation( @@ -291,7 +292,7 @@ async def _add_automations(config): await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_SHUTDOWN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY)) await cg.register_component(trigger, conf) await automation.build_automation(trigger, [], conf) diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index 04658d567c..b953a95664 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -9,7 +9,7 @@ namespace esphome { ESP_LOGCONFIG(TAG, prefix "%s", (pin)->dump_summary().c_str()); \ } -// put GPIO flags in a namepsace to not pollute esphome namespace +// put GPIO flags in a namespace to not pollute esphome namespace namespace gpio { enum Flags : uint8_t { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index b03d890ad8..d82e452c3d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -258,6 +258,19 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { return std::string(tmp); } +int8_t step_to_accuracy_decimals(float step) { + // use printf %g to find number of digits based on temperature step + char buf[32]; + sprintf(buf, "%.5g", step); + + std::string str{buf}; + size_t dot_pos = str.find('.'); + if (dot_pos == std::string::npos) + return 0; + + return str.length() - dot_pos - 1; +} + // Colors float gamma_correct(float value, float gamma) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8c723a04cb..6bed743010 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -258,10 +258,10 @@ inline std::string to_string(const std::string &val) { return val; } /// Truncate a string to a specific length. std::string str_truncate(const std::string &str, size_t length); -/// Extract the part of the string until either the first occurence of the specified character, or the end (requires str -/// to be null-terminated). +/// Extract the part of the string until either the first occurrence of the specified character, or the end +/// (requires str to be null-terminated). std::string str_until(const char *str, char ch); -/// Extract the part of the string until either the first occurence of the specified character, or the end. +/// Extract the part of the string until either the first occurrence of the specified character, or the end. std::string str_until(const std::string &str, char ch); /// Convert the string to lower case. @@ -415,6 +415,9 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +/// Derive accuracy in decimals from an increment step. +int8_t step_to_accuracy_decimals(float step); + ///@} /// @name Colors diff --git a/esphome/espota2.py b/esphome/espota2.py index 8f299395dd..76f3b917c9 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -60,7 +60,6 @@ class ProgressBar: sys.stderr.write(text) sys.stderr.flush() - # pylint: disable=no-self-use def done(self): sys.stderr.write("\n") sys.stderr.flush() diff --git a/esphome/helpers.py b/esphome/helpers.py index 76158a1bfd..e958aca78e 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -296,7 +296,7 @@ _TYPE_OVERLOADS = { int: type("EInt", (int,), {}), float: type("EFloat", (float,), {}), str: type("EStr", (str,), {}), - dict: type("EDict", (str,), {}), + dict: type("EDict", (dict,), {}), list: type("EList", (list,), {}), } diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 2072e25ec5..c4bf3d3f1a 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -20,7 +20,7 @@ def patch_structhash(): # removed/added. This might have unintended consequences, but this improves compile # times greatly when adding/removing components and a simple clean build solves # all issues - from platformio.commands.run import helpers, command + from platformio.run import helpers, cli from os.path import join, isdir, getmtime from os import makedirs @@ -39,7 +39,7 @@ def patch_structhash(): # pylint: disable=protected-access helpers.clean_build_dir = patched_clean_build_dir - command.clean_build_dir = patched_clean_build_dir + cli.clean_build_dir = patched_clean_build_dir IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" diff --git a/esphome/jsonschema.py b/esphome/schema_extractors.py similarity index 75% rename from esphome/jsonschema.py rename to esphome/schema_extractors.py index 94325f4abc..2280a84849 100644 --- a/esphome/jsonschema.py +++ b/esphome/schema_extractors.py @@ -9,9 +9,9 @@ However there is a property to further disable decorator impact.""" -# This is set to true by script/build_jsonschema.py +# This is set to true by script/build_language_schema.py # only, so data is collected (again functionality is not modified) -EnableJsonSchemaCollect = False +EnableSchemaExtraction = False extended_schemas = {} list_schemas = {} @@ -19,9 +19,12 @@ registry_schemas = {} hidden_schemas = {} typed_schemas = {} +# This key is used to generate schema files of Esphome configuration. +SCHEMA_EXTRACT = object() -def jschema_extractor(validator_name): - if EnableJsonSchemaCollect: + +def schema_extractor(validator_name): + if EnableSchemaExtraction: def decorator(func): hidden_schemas[repr(func)] = validator_name @@ -35,8 +38,8 @@ def jschema_extractor(validator_name): return dummy -def jschema_extended(func): - if EnableJsonSchemaCollect: +def schema_extractor_extended(func): + if EnableSchemaExtraction: def decorate(*args, **kwargs): ret = func(*args, **kwargs) @@ -49,8 +52,8 @@ def jschema_extended(func): return func -def jschema_list(func): - if EnableJsonSchemaCollect: +def schema_extractor_list(func): + if EnableSchemaExtraction: def decorate(*args, **kwargs): ret = func(*args, **kwargs) @@ -63,8 +66,8 @@ def jschema_list(func): return func -def jschema_registry(registry): - if EnableJsonSchemaCollect: +def schema_extractor_registry(registry): + if EnableSchemaExtraction: def decorator(func): registry_schemas[repr(func)] = registry @@ -78,8 +81,8 @@ def jschema_registry(registry): return dummy -def jschema_typed(func): - if EnableJsonSchemaCollect: +def schema_extractor_typed(func): + if EnableSchemaExtraction: def decorate(*args, **kwargs): ret = func(*args, **kwargs) diff --git a/esphome/util.py b/esphome/util.py index 9975f5fc72..927c50fe89 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -152,7 +152,6 @@ class RedirectText: # any caller. return len(s) - # pylint: disable=no-self-use def isatty(self): return True diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 0fdae423cf..f4ed2fe03b 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -2,7 +2,7 @@ import difflib import itertools import voluptuous as vol -from esphome.jsonschema import jschema_extended +from esphome.schema_extractors import schema_extractor_extended class ExtraKeysInvalid(vol.Invalid): @@ -203,7 +203,7 @@ class _Schema(vol.Schema): self._extra_schemas.append(validator) return self - @jschema_extended + @schema_extractor_extended # pylint: disable=signature-differs def extend(self, *schemas, **kwargs): extra = kwargs.pop("extra", None) diff --git a/esphome/wizard.py b/esphome/wizard.py index 469219300b..602f4ecf04 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -343,7 +343,7 @@ def wizard(path): sleep(1) safe_print_step(3, WIFI_BIG) - safe_print("In this step, I'm going to create the configuration for " "WiFi.") + safe_print("In this step, I'm going to create the configuration for WiFi.") safe_print() sleep(1) safe_print( diff --git a/platformio.ini b/platformio.ini index 733e942a84..9b943242f7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -62,7 +62,7 @@ lib_deps = mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.5 ; dsmr - rweather/Crypto@0.2.0 ; dsmr + rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.8 ; midea tonia/HeatpumpIR@1.0.20 ; heatpumpir build_flags = diff --git a/pylintrc b/pylintrc index 8f2e9a7359..b70e5c7da9 100644 --- a/pylintrc +++ b/pylintrc @@ -24,7 +24,6 @@ disable= undefined-loop-variable, useless-object-inheritance, stop-iteration-return, - no-self-use, import-outside-toplevel, # Broken unsupported-membership-test, diff --git a/requirements.txt b/requirements.txt index dfe69cd33a..f31b1662bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ voluptuous==0.13.1 PyYAML==6.0 paho-mqtt==1.6.1 -colorama==0.4.4 +colorama==0.4.5 tornado==6.1 tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==5.2.5 # When updating platformio, also update Dockerfile +platformio==6.0.2 # When updating platformio, also update Dockerfile esptool==3.3.1 click==8.1.3 esphome-dashboard==20220508.0 -aioesphomeapi==10.8.2 -zeroconf==0.38.4 +aioesphomeapi==10.11.0 +zeroconf==0.39.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/requirements_test.txt b/requirements_test.txt index 7c21979647..ed48818276 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ -pylint==2.13.9 -flake8==4.0.1 -black==22.3.0 -pyupgrade==2.32.1 +pylint==2.14.5 +flake8==5.0.4 +black==22.6.0 # also change in .pre-commit-config.yaml when updating +pyupgrade==2.37.3 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests pytest==7.1.1 pytest-cov==3.0.0 -pytest-mock==3.7.0 -pytest-asyncio==0.18.3 +pytest-mock==3.8.2 +pytest-asyncio==0.19.0 asyncmock==0.4.2 hypothesis==5.49.0 diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py deleted file mode 100644 index 5373d404a7..0000000000 --- a/script/build_jsonschema.py +++ /dev/null @@ -1,828 +0,0 @@ -#!/usr/bin/env python3 - -from esphome.cpp_generator import MockObj -import json -import argparse -import os -import re -from pathlib import Path -import voluptuous as vol - -# NOTE: Cannot import other esphome components globally as a modification in jsonschema -# is needed before modules are loaded -import esphome.jsonschema as ejs - -ejs.EnableJsonSchemaCollect = True - -DUMP_COMMENTS = False - -JSC_ACTION = "automation.ACTION_REGISTRY" -JSC_ALLOF = "allOf" -JSC_ANYOF = "anyOf" -JSC_COMMENT = "$comment" -JSC_CONDITION = "automation.CONDITION_REGISTRY" -JSC_DESCRIPTION = "description" -JSC_ONEOF = "oneOf" -JSC_PROPERTIES = "properties" -JSC_REF = "$ref" - -# this should be required, but YAML Language server completion does not work properly if required are specified. -# still needed for other features / checks -JSC_REQUIRED = "required_" - -SIMPLE_AUTOMATION = "simple_automation" - -schema_names = {} -schema_registry = {} -components = {} -modules = {} -registries = [] -pending_refs = [] - -definitions = {} -base_props = {} - - -parser = argparse.ArgumentParser() -parser.add_argument( - "--output", default="esphome.json", help="Output filename", type=os.path.abspath -) - -args = parser.parse_args() - - -def get_ref(definition): - return {JSC_REF: "#/definitions/" + definition} - - -def is_ref(jschema): - return isinstance(jschema, dict) and JSC_REF in jschema - - -def unref(jschema): - return definitions.get(jschema[JSC_REF][len("#/definitions/") :]) - - -def add_definition_array_or_single_object(ref): - return {JSC_ANYOF: [{"type": "array", "items": ref}, ref]} - - -def add_core(): - from esphome.core.config import CONFIG_SCHEMA - - base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA) - - -def add_buses(): - # uart - from esphome.components.uart import UART_DEVICE_SCHEMA - - get_jschema("uart_bus", UART_DEVICE_SCHEMA) - - # spi - from esphome.components.spi import spi_device_schema - - get_jschema("spi_bus", spi_device_schema(False)) - - # i2c - from esphome.components.i2c import i2c_device_schema - - get_jschema("i2c_bus", i2c_device_schema(None)) - - -def add_registries(): - for domain, module in modules.items(): - add_module_registries(domain, module) - - -def add_module_registries(domain, module): - from esphome.util import Registry - - for c in dir(module): - m = getattr(module, c) - if isinstance(m, Registry): - add_registry(domain + "." + c, m) - - -def add_registry(registry_name, registry): - validators = [] - registries.append((registry, registry_name)) - for name in registry.keys(): - schema = get_jschema(str(name), registry[name].schema, create_return_ref=False) - if not schema: - schema = {"type": "null"} - o_schema = {"type": "object", JSC_PROPERTIES: {name: schema}} - o_schema = create_ref( - registry_name + "-" + name, str(registry[name].schema) + "x", o_schema - ) - validators.append(o_schema) - definitions[registry_name] = {JSC_ANYOF: validators} - - -def get_registry_ref(registry): - # we don't know yet - ref = {JSC_REF: "pending"} - pending_refs.append((ref, registry)) - return ref - - -def solve_pending_refs(): - for ref, registry in pending_refs: - for registry_match, name in registries: - if registry == registry_match: - ref[JSC_REF] = "#/definitions/" + name - - -def add_module_schemas(name, module): - import esphome.config_validation as cv - - for c in dir(module): - v = getattr(module, c) - if isinstance(v, cv.Schema): - get_jschema(name + "." + c, v) - - -def get_dirs(): - from esphome.loader import CORE_COMPONENTS_PATH - - dir_names = [ - d - for d in os.listdir(CORE_COMPONENTS_PATH) - if not d.startswith("__") - and os.path.isdir(os.path.join(CORE_COMPONENTS_PATH, d)) - ] - return dir_names - - -def get_logger_tags(): - from esphome.loader import CORE_COMPONENTS_PATH - import glob - - pattern = re.compile(r'^static const char(\*\s|\s\*)TAG = "(\w.*)";', re.MULTILINE) - tags = [ - "app", - "component", - "esphal", - "helpers", - "preferences", - "scheduler", - "api.service", - ] - for x in os.walk(CORE_COMPONENTS_PATH): - for y in glob.glob(os.path.join(x[0], "*.cpp")): - with open(y) as file: - data = file.read() - match = pattern.search(data) - if match: - tags.append(match.group(2)) - return tags - - -def load_components(): - import esphome.config_validation as cv - from esphome.config import get_component - - modules["cv"] = cv - from esphome import automation - - modules["automation"] = automation - - for domain in get_dirs(): - components[domain] = get_component(domain) - modules[domain] = components[domain].module - - -def add_components(): - from esphome.config import get_platform - - for domain, c in components.items(): - if c.is_platform_component: - # this is a platform_component, e.g. binary_sensor - platform_schema = [ - { - "type": "object", - "properties": {"platform": {"type": "string"}}, - } - ] - if domain not in ("output", "display"): - # output bases are either FLOAT or BINARY so don't add common base for this - # display bases are either simple or FULL so don't add common base for this - platform_schema = [ - {"$ref": f"#/definitions/{domain}.{domain.upper()}_SCHEMA"} - ] + platform_schema - - base_props[domain] = {"type": "array", "items": {"allOf": platform_schema}} - - add_module_registries(domain, c.module) - add_module_schemas(domain, c.module) - - # need first to iterate all platforms then iterate components - # a platform component can have other components as properties, - # e.g. climate components usually have a temperature sensor - - for domain, c in components.items(): - if (c.config_schema is not None) or c.is_platform_component: - if c.is_platform_component: - platform_schema = base_props[domain]["items"]["allOf"] - for platform in get_dirs(): - p = get_platform(domain, platform) - if p is not None: - # this is a platform element, e.g. - # - platform: gpio - schema = get_jschema( - domain + "-" + platform, - p.config_schema, - create_return_ref=False, - ) - if ( - schema - ): # for invalid schemas, None is returned thus is deprecated - platform_schema.append( - { - "if": { - JSC_PROPERTIES: { - "platform": {"const": platform} - } - }, - "then": schema, - } - ) - - elif c.config_schema is not None: - # adds root components which are not platforms, e.g. api: logger: - if c.multi_conf: - schema = get_jschema(domain, c.config_schema) - schema = add_definition_array_or_single_object(schema) - else: - schema = get_jschema(domain, c.config_schema, False) - base_props[domain] = schema - - -def get_automation_schema(name, vschema): - from esphome.automation import AUTOMATION_SCHEMA - - # ensure SIMPLE_AUTOMATION - if SIMPLE_AUTOMATION not in definitions: - simple_automation = add_definition_array_or_single_object(get_ref(JSC_ACTION)) - simple_automation[JSC_ANYOF].append( - get_jschema(AUTOMATION_SCHEMA.__module__, AUTOMATION_SCHEMA) - ) - - definitions[schema_names[str(AUTOMATION_SCHEMA)]][JSC_PROPERTIES][ - "then" - ] = add_definition_array_or_single_object(get_ref(JSC_ACTION)) - definitions[SIMPLE_AUTOMATION] = simple_automation - - extra_vschema = None - if AUTOMATION_SCHEMA == ejs.extended_schemas[str(vschema)][0]: - extra_vschema = ejs.extended_schemas[str(vschema)][1] - - if not extra_vschema: - return get_ref(SIMPLE_AUTOMATION) - - # add then property - extra_jschema = get_jschema(name, extra_vschema, False) - - if is_ref(extra_jschema): - return extra_jschema - - if not JSC_PROPERTIES in extra_jschema: - # these are interval: and exposure_notifications, featuring automations a component - extra_jschema[JSC_ALLOF][0][JSC_PROPERTIES][ - "then" - ] = add_definition_array_or_single_object(get_ref(JSC_ACTION)) - ref = create_ref(name, extra_vschema, extra_jschema) - return add_definition_array_or_single_object(ref) - - # automations can be either - # * a single action, - # * an array of action, - # * an object with automation's schema and a then key - # with again a single action or an array of actions - - if len(extra_jschema[JSC_PROPERTIES]) == 0: - return get_ref(SIMPLE_AUTOMATION) - - extra_jschema[JSC_PROPERTIES]["then"] = add_definition_array_or_single_object( - get_ref(JSC_ACTION) - ) - # if there is a required element in extra_jschema then this automation does not support - # directly a list of actions - if JSC_REQUIRED in extra_jschema: - return create_ref(name, extra_vschema, extra_jschema) - - jschema = add_definition_array_or_single_object(get_ref(JSC_ACTION)) - jschema[JSC_ANYOF].append(extra_jschema) - - return create_ref(name, extra_vschema, jschema) - - -def get_entry(parent_key, vschema): - from esphome.voluptuous_schema import _Schema as schema_type - - entry = {} - # annotate schema validator info - if DUMP_COMMENTS: - entry[JSC_COMMENT] = "entry: " + parent_key + "/" + str(vschema) - - if isinstance(vschema, dict): - entry = {"what": "is_this"} - elif isinstance(vschema, list): - ref = get_jschema(parent_key + "[]", vschema[0]) - entry = {"type": "array", "items": ref} - elif isinstance(vschema, schema_type) and hasattr(vschema, "schema"): - entry = get_jschema(parent_key, vschema, False) - elif hasattr(vschema, "validators"): - entry = get_jschema(parent_key, vschema, False) - elif vschema in schema_registry: - entry = schema_registry[vschema].copy() - elif str(vschema) in ejs.registry_schemas: - entry = get_registry_ref(ejs.registry_schemas[str(vschema)]) - elif str(vschema) in ejs.list_schemas: - ref = get_jschema(parent_key, ejs.list_schemas[str(vschema)][0]) - entry = {JSC_ANYOF: [ref, {"type": "array", "items": ref}]} - elif str(vschema) in ejs.typed_schemas: - schema_types = [{"type": "object", "properties": {"type": {"type": "string"}}}] - entry = {"allOf": schema_types} - for schema_key, vschema_type in ejs.typed_schemas[str(vschema)][0][0].items(): - schema_types.append( - { - "if": {"properties": {"type": {"const": schema_key}}}, - "then": get_jschema(f"{parent_key}-{schema_key}", vschema_type), - } - ) - - elif str(vschema) in ejs.hidden_schemas: - # get the schema from the automation schema - type = ejs.hidden_schemas[str(vschema)] - inner_vschema = vschema(ejs.jschema_extractor) - if type == "automation": - entry = get_automation_schema(parent_key, inner_vschema) - elif type == "maybe": - entry = get_jschema(parent_key, inner_vschema) - elif type == "one_of": - entry = {"enum": list(inner_vschema)} - elif type == "enum": - entry = {"enum": list(inner_vschema.keys())} - elif type == "effects": - # Like list schema but subset from list. - subset_list = inner_vschema[0] - # get_jschema('strobex', registry['strobe'].schema) - registry_schemas = [] - for name in subset_list: - registry_schemas.append(get_ref("light.EFFECTS_REGISTRY-" + name)) - - entry = { - JSC_ANYOF: [{"type": "array", "items": {JSC_ANYOF: registry_schemas}}] - } - - else: - raise ValueError("Unknown extracted schema type") - elif str(vschema).startswith(" 0: - output[JSC_REQUIRED] = required - return output - - -def add_pin_schema(): - from esphome import pins - - add_module_schemas("PIN", pins) - - -def add_pin_registry(): - from esphome import pins - - pin_registry = pins.PIN_SCHEMA_REGISTRY - assert len(pin_registry) > 0 - # Here are schemas for pcf8574, mcp23xxx and other port expanders which add - # gpio registers - # ESPHome validates pins schemas if it founds a key in the pin configuration. - # This key is added to a required in jsonschema, and all options are part of a - # oneOf section, so only one is selected. Also internal schema adds number as required. - - for mode in ("INPUT", "OUTPUT"): - schema_name = f"PIN.GPIO_FULL_{mode}_PIN_SCHEMA" - - # TODO: get pin definitions properly - if schema_name not in definitions: - definitions[schema_name] = {"type": ["object", "null"], JSC_PROPERTIES: {}} - - internal = definitions[schema_name] - definitions[schema_name]["additionalItems"] = False - definitions[f"PIN.{mode}_INTERNAL"] = internal - internal[JSC_PROPERTIES]["number"] = {"type": ["number", "string"]} - schemas = [get_ref(f"PIN.{mode}_INTERNAL")] - schemas[0]["required"] = ["number"] - # accept string and object, for internal shorthand pin IO: - definitions[schema_name] = {"oneOf": schemas, "type": ["string", "object"]} - - for k, v in pin_registry.items(): - if isinstance(v[1], vol.validators.All): - pin_jschema = get_jschema(f"PIN.{mode}_" + k, v[1]) - if unref(pin_jschema): - pin_jschema["required"] = [k] - schemas.append(pin_jschema) - - -def dump_schema(): - import esphome.config_validation as cv - - from esphome import automation - from esphome.automation import validate_potentially_and_condition - from esphome import pins - from esphome.core import CORE - from esphome.helpers import write_file_if_changed - from esphome.components import remote_base - - # The root directory of the repo - root = Path(__file__).parent.parent - - # Fake some directory so that get_component works - CORE.config_path = str(root) - - file_path = args.output - - schema_registry[cv.boolean] = {"type": "boolean"} - - for v in [ - cv.int_, - cv.int_range, - cv.positive_int, - cv.float_, - cv.positive_float, - cv.positive_float, - cv.positive_not_null_int, - cv.negative_one_to_one_float, - cv.port, - ]: - schema_registry[v] = {"type": "number"} - - for v in [ - cv.string, - cv.string_strict, - cv.valid_name, - cv.hex_int, - cv.hex_int_range, - pins.gpio_output_pin_schema, - pins.gpio_input_pin_schema, - pins.gpio_input_pullup_pin_schema, - cv.float_with_unit, - cv.subscribe_topic, - cv.publish_topic, - cv.mqtt_payload, - cv.ssid, - cv.percentage_int, - cv.percentage, - cv.possibly_negative_percentage, - cv.positive_time_period, - cv.positive_time_period_microseconds, - cv.positive_time_period_milliseconds, - cv.positive_time_period_minutes, - cv.positive_time_period_seconds, - ]: - schema_registry[v] = {"type": "string"} - - schema_registry[validate_potentially_and_condition] = get_ref("condition_list") - - for v in [pins.gpio_input_pin_schema, pins.gpio_input_pullup_pin_schema]: - schema_registry[v] = get_ref("PIN.GPIO_FULL_INPUT_PIN_SCHEMA") - for v in [pins.internal_gpio_input_pin_schema, pins.gpio_input_pin_schema]: - schema_registry[v] = get_ref("PIN.INPUT_INTERNAL") - - for v in [pins.gpio_output_pin_schema, pins.internal_gpio_output_pin_schema]: - schema_registry[v] = get_ref("PIN.GPIO_FULL_OUTPUT_PIN_SCHEMA") - for v in [pins.internal_gpio_output_pin_schema, pins.gpio_output_pin_schema]: - schema_registry[v] = get_ref("PIN.OUTPUT_INTERNAL") - - add_module_schemas("CONFIG", cv) - get_jschema("POLLING_COMPONENT", cv.polling_component_schema("60s")) - - add_pin_schema() - - add_module_schemas("REMOTE_BASE", remote_base) - add_module_schemas("AUTOMATION", automation) - - load_components() - add_registries() - - definitions["condition_list"] = { - JSC_ONEOF: [ - {"type": "array", "items": get_ref(JSC_CONDITION)}, - get_ref(JSC_CONDITION), - ] - } - - output = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "definitions": definitions, - JSC_PROPERTIES: base_props, - } - - add_core() - add_buses() - add_components() - - add_registries() # need second pass, e.g. climate.pid.autotune - add_pin_registry() - solve_pending_refs() - - write_file_if_changed(file_path, json.dumps(output)) - print(f"Wrote {file_path}") - - -dump_schema() diff --git a/script/build_language_schema.py b/script/build_language_schema.py index ec0912e9e6..0b3cdf976d 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1,18 +1,19 @@ import inspect import json import argparse -from operator import truediv import os +import glob +import re import voluptuous as vol -# NOTE: Cannot import other esphome components globally as a modification in jsonschema +# NOTE: Cannot import other esphome components globally as a modification in vol_schema # is needed before modules are loaded -import esphome.jsonschema as ejs +import esphome.schema_extractors as ejs -ejs.EnableJsonSchemaCollect = True +ejs.EnableSchemaExtraction = True # schema format: -# Schemas are splitted in several files in json format, one for core stuff, one for each platform (sensor, binary_sensor, etc) and +# Schemas are split in several files in json format, one for core stuff, one for each platform (sensor, binary_sensor, etc) and # one for each component (dallas, sim800l, etc.) component can have schema for root component/hub and also for platform component, # e.g. dallas has hub component which has pin and then has the sensor platform which has sensor name, index, etc. # When files are loaded they are merged in a single object. @@ -60,15 +61,6 @@ solve_registry = [] def get_component_names(): - # return [ - # "esphome", - # "esp32", - # "esp8266", - # "logger", - # "sensor", - # "remote_receiver", - # "binary_sensor", - # ] from esphome.loader import CORE_COMPONENTS_PATH component_names = ["esphome", "sensor"] @@ -100,7 +92,7 @@ from esphome import automation from esphome import pins from esphome.components import remote_base from esphome.const import CONF_TYPE -from esphome.loader import get_platform +from esphome.loader import get_platform, CORE_COMPONENTS_PATH from esphome.helpers import write_file_if_changed from esphome.util import Registry @@ -120,10 +112,12 @@ def write_file(name, obj): def register_module_schemas(key, module, manifest=None): for name, schema in module_schemas(module): register_known_schema(key, name, schema) - if ( - manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS] - ): # not sure about 2nd part of the if, might be useless config (e.g. as3935) - output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + + if manifest: + # Multi conf should allow list of components + # not sure about 2nd part of the if, might be useless config (e.g. as3935) + if manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: + output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True def register_known_schema(module, name, schema): @@ -265,13 +259,58 @@ def do_esp8266(): def fix_remote_receiver(): - output["remote_receiver.binary_sensor"]["schemas"]["CONFIG_SCHEMA"] = { + remote_receiver_schema = output["remote_receiver.binary_sensor"]["schemas"] + remote_receiver_schema["CONFIG_SCHEMA"] = { "type": "schema", "schema": { "extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], - "config_vars": output["remote_base"]["binary"], + "config_vars": output["remote_base"].pop("binary"), }, } + remote_receiver_schema["CONFIG_SCHEMA"]["schema"]["config_vars"]["receiver_id"] = { + "key": "GeneratedID", + "use_id_type": "remote_base::RemoteReceiverBase", + "type": "use_id", + } + + +def fix_script(): + output["script"][S_SCHEMAS][S_CONFIG_SCHEMA][S_TYPE] = S_SCHEMA + config_schema = output["script"][S_SCHEMAS][S_CONFIG_SCHEMA] + config_schema[S_SCHEMA][S_CONFIG_VARS]["id"]["id_type"] = { + "class": "script::Script" + } + config_schema["is_list"] = True + + +def get_logger_tags(): + pattern = re.compile(r'^static const char \*const TAG = "(\w.*)";', re.MULTILINE) + # tags not in components dir + tags = [ + "app", + "component", + "entity_base", + "scheduler", + "api.service", + ] + for x in os.walk(CORE_COMPONENTS_PATH): + for y in glob.glob(os.path.join(x[0], "*.cpp")): + with open(y, encoding="utf-8") as file: + data = file.read() + match = pattern.search(data) + if match: + tags.append(match.group(1)) + return tags + + +def add_logger_tags(): + tags = get_logger_tags() + logs = output["logger"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"][ + "logs" + ]["schema"]["config_vars"] + for t in tags: + logs[t] = logs["string"].copy() + logs.pop("string") def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=False): @@ -401,7 +440,7 @@ def shrink(): else: print("expected extends here!" + x) arr_s = merge(key_s, arr_s) - if arr_s[S_TYPE] == "enum": + if arr_s[S_TYPE] in ["enum", "typed"]: arr_s.pop(S_SCHEMA) else: arr_s.pop(S_EXTENDS) @@ -491,14 +530,20 @@ def build_schema(): if domain not in platforms: if manifest.config_schema is not None: core_components[domain] = {} + if len(manifest.dependencies) > 0: + core_components[domain]["dependencies"] = manifest.dependencies register_module_schemas(domain, manifest.module, manifest) for platform in platforms: platform_manifest = get_platform(domain=platform, platform=domain) if platform_manifest is not None: output[platform][S_COMPONENTS][domain] = {} + if len(platform_manifest.dependencies) > 0: + output[platform][S_COMPONENTS][domain][ + "dependencies" + ] = platform_manifest.dependencies register_module_schemas( - f"{domain}.{platform}", platform_manifest.module + f"{domain}.{platform}", platform_manifest.module, platform_manifest ) # Do registries @@ -517,6 +562,8 @@ def build_schema(): do_esp8266() do_esp32() fix_remote_receiver() + fix_script() + add_logger_tags() shrink() # aggregate components, so all component info is in same file, otherwise we have dallas.json, dallas.sensor.json, etc. @@ -585,7 +632,7 @@ def convert_1(schema, config_var, path): assert S_EXTENDS not in config_var if not S_TYPE in config_var: config_var[S_TYPE] = S_SCHEMA - assert config_var[S_TYPE] == S_SCHEMA + # assert config_var[S_TYPE] == S_SCHEMA if S_SCHEMA not in config_var: config_var[S_SCHEMA] = {} @@ -662,7 +709,7 @@ def convert_1(schema, config_var, path): elif repr_schema in ejs.hidden_schemas: schema_type = ejs.hidden_schemas[repr_schema] - data = schema(ejs.jschema_extractor) + data = schema(ejs.SCHEMA_EXTRACT) # enums, e.g. esp32/variant if schema_type == "one_of": @@ -672,8 +719,9 @@ def convert_1(schema, config_var, path): config_var[S_TYPE] = "enum" config_var["values"] = list(data.keys()) elif schema_type == "maybe": - config_var[S_TYPE] = "maybe" - config_var["schema"] = convert_config(data, path + "/maybe")["schema"] + config_var[S_TYPE] = S_SCHEMA + config_var["maybe"] = data[1] + config_var["schema"] = convert_config(data[0], path + "/maybe")["schema"] # esphome/on_boot elif schema_type == "automation": extra_schema = None @@ -717,8 +765,50 @@ def convert_1(schema, config_var, path): elif schema_type == "sensor": schema = data convert_1(data, config_var, path + "/trigger") + elif schema_type == "declare_id": + # pylint: disable=protected-access + parents = data._parents + + config_var["id_type"] = { + "class": str(data.base), + "parents": [str(x.base) for x in parents] + if isinstance(parents, list) + else None, + } + elif schema_type == "use_id": + if inspect.ismodule(data): + m_attr_obj = getattr(data, "CONFIG_SCHEMA") + use_schema = known_schemas.get(repr(m_attr_obj)) + if use_schema: + [output_module, output_name] = use_schema[0][1].split(".") + use_id_config = output[output_module][S_SCHEMAS][output_name] + config_var["use_id_type"] = use_id_config["schema"]["config_vars"][ + "id" + ]["id_type"]["class"] + config_var[S_TYPE] = "use_id" + else: + print("TODO deferred?") + else: + if isinstance(data, str): + # TODO: Figure out why pipsolar does this + config_var["use_id_type"] = data + else: + config_var["use_id_type"] = str(data.base) + config_var[S_TYPE] = "use_id" else: raise Exception("Unknown extracted schema type") + elif config_var.get("key") == "GeneratedID": + if path == "i2c/CONFIG_SCHEMA/extL/all/id": + config_var["id_type"] = {"class": "i2c::I2CBus", "parents": ["Component"]} + elif path == "uart/CONFIG_SCHEMA/val 1/extL/all/id": + config_var["id_type"] = { + "class": "uart::UARTComponent", + "parents": ["Component"], + } + elif path == "pins/esp32/val 1/id": + config_var["id_type"] = "pin" + else: + raise Exception("Cannot determine id_type for " + path) elif repr_schema in ejs.registry_schemas: solve_registry.append((ejs.registry_schemas[repr_schema], config_var)) @@ -787,7 +877,13 @@ def convert_keys(converted, schema, path): result["key"] = "Optional" else: converted["key"] = "String" - converted["key_dump"] = str(k) + key_string_match = re.search( + r"", str(k), re.IGNORECASE + ) + if key_string_match: + converted["key_type"] = key_string_match.group(1) + else: + converted["key_type"] = str(k) esphome_core.CORE.data = { esphome_core.KEY_CORE: {esphome_core.KEY_TARGET_PLATFORM: "esp8266"} @@ -808,6 +904,12 @@ def convert_keys(converted, schema, path): if base_k in result and base_v == result[base_k]: result.pop(base_k) converted["schema"][S_CONFIG_VARS][str(k)] = result + if "key" in converted and converted["key"] == "String": + config_vars = converted["schema"]["config_vars"] + assert len(config_vars) == 1 + key = list(config_vars.keys())[0] + assert key.startswith("<") + config_vars["string"] = config_vars.pop(key) build_schema() diff --git a/script/ci-custom.py b/script/ci-custom.py index c0737da103..6f69b55d2c 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -452,7 +452,7 @@ def lint_no_removed_in_idf_conversions(fname, match): replacement = IDF_CONVERSION_FORBIDDEN[match.group(1)] return ( f"The macro {highlight(match.group(1))} can no longer be used in ESPHome directly. " - f"Plese use {highlight(replacement)} instead." + f"Please use {highlight(replacement)} instead." ) diff --git a/tests/test1.yaml b/tests/test1.yaml index 2a157e3513..5897639ead 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -301,6 +301,10 @@ ble_client: - switch.turn_on: ble1_status - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client +bedjet: + - ble_client_id: my_bedjet_ble_client + id: my_bedjet_client + time_id: sntp_time mcp23s08: - id: "mcp23s08_hub" cs_pin: GPIO12 @@ -600,7 +604,6 @@ sensor: sensor: hlw8012_power name: "Integration Sensor lazy" time_unit: s - min_save_interval: 60s - platform: hmc5883l address: 0x68 field_strength_x: @@ -1138,7 +1141,7 @@ sensor: - platform: max9611 i2c_id: i2c_bus shunt_resistance: 0.2 ohm - gain: '1X' + gain: "1X" voltage: name: Max9611 Voltage current: @@ -1149,7 +1152,6 @@ sensor: name: Max9611 Temp update_interval: 1s - esp32_touch: setup_mode: False iir_filter: 10ms @@ -1380,6 +1382,19 @@ binary_sensor: threshold: 100 filters: - invert: + - platform: template + id: open_endstop_sensor + - platform: template + id: open_sensor + - platform: template + id: open_obstacle_sensor + + - platform: template + id: close_endstop_sensor + - platform: template + id: close_sensor + - platform: template + id: close_obstacle_sensor pca9685: frequency: 500 @@ -1892,8 +1907,10 @@ climate: icon: mdi:stove - platform: bedjet name: My Bedjet - ble_client_id: my_bedjet_ble_client + bedjet_id: my_bedjet_client heat_mode: extended + - platform: whynter + name: Whynter script: - id: climate_custom @@ -2367,6 +2384,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7789v + model: TTGO TDisplay 135x240 cs_pin: GPIO5 dc_pin: GPIO16 reset_pin: GPIO23 @@ -2540,6 +2558,36 @@ cover: id: am43_test ble_client_id: ble_foo icon: mdi:blinds + - platform: feedback + name: "Feedback Cover" + id: gate + device_class: gate + + infer_endstop_from_movement: false + has_built_in_endstop: false + max_duration: 30s + direction_change_wait_time: 300ms + acceleration_wait_time: 150ms + obstacle_rollback: 10% + + open_duration: 22.1s + open_endstop: open_endstop_sensor + open_sensor: open_sensor + open_obstacle_sensor: open_obstacle_sensor + + close_duration: 22.4s + close_endstop: close_endstop_sensor + close_sensor: close_sensor + close_obstacle_sensor: close_obstacle_sensor + + open_action: + - logger.log: Open Action + + close_action: + - logger.log: Close Action + + stop_action: + - logger.log: Stop Action debug: @@ -2611,10 +2659,10 @@ globals: text_sensor: - platform: ble_client ble_client_id: ble_foo - name: 'Sensor Location' - service_uuid: '180d' - characteristic_uuid: '2a38' - descriptor_uuid: '2902' + name: "Sensor Location" + service_uuid: "180d" + characteristic_uuid: "2a38" + descriptor_uuid: "2902" notify: true update_interval: never on_notify: @@ -2706,7 +2754,7 @@ canbus: lambda: "return x[0] == 0x11;" then: light.toggle: ${roomname}_lights - - can_id: 0b00000000000000000000001000000 + - can_id: 0b00000000000000000000001000000 can_id_mask: 0b11111000000000011111111000000 use_extended_id: true then: @@ -2744,7 +2792,7 @@ canbus: lambda: "return x[0] == 0x11;" then: light.toggle: ${roomname}_lights - - can_id: 0b00000000000000000000001000000 + - can_id: 0b00000000000000000000001000000 can_id_mask: 0b11111000000000011111111000000 use_extended_id: true then: diff --git a/tests/test2.yaml b/tests/test2.yaml index f88486524f..110e8e6625 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -605,3 +605,19 @@ cap1188: touch_threshold: 0x20 allow_multiple_touches: true reset_pin: 14 + +switch: + - platform: template + name: "Test BLE Write Action" + turn_on_action: + - ble_client.ble_write: + id: airthings01 + service_uuid: F61E3BE9-2826-A81B-970A-4D4DECFABBAE + characteristic_uuid: 6490FAFE-0734-732C-8705-91B653A081FC + value: [0x01, 0xab, 0xff] + - ble_client.ble_write: + id: airthings01 + service_uuid: F61E3BE9-2826-A81B-970A-4D4DECFABBAE + characteristic_uuid: 6490FAFE-0734-732C-8705-91B653A081FC + value: !lambda |- + return {0x13, 0x37}; diff --git a/tests/test3.yaml b/tests/test3.yaml index af0e784465..1abbee8dc5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -270,6 +270,10 @@ uart: tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 + - id: uart10 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 modbus: uart_id: uart1 @@ -755,6 +759,19 @@ sensor: temperature: name: "mlxtemp" oversampling: 2 + - platform: smt100 + uart_id: uart10 + counts: + name: "Counts" + dielectric_constant: + name: "Dielectric Constant" + temperature: + name: "Temperature" + moisture: + name: "Moisture" + voltage: + name: "Voltage" + update_interval: 60s time: - platform: homeassistant @@ -1111,6 +1128,49 @@ climate: ki: 0.0 kd: 0.0 +sprinkler: + - id: yard_sprinkler_ctrlr + main_switch: "Yard Sprinklers" + auto_advance_switch: "Yard Sprinklers Auto Advance" + reverse_switch: "Yard Sprinklers Reverse" + pump_start_pump_delay: 2s + pump_stop_valve_delay: 4s + pump_switch_off_during_valve_open_delay: true + valve_open_delay: 5s + valves: + - valve_switch: "Yard Valve 0" + enable_switch: "Enable Yard Valve 0" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - valve_switch: "Yard Valve 1" + enable_switch: "Enable Yard Valve 1" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - valve_switch: "Yard Valve 2" + enable_switch: "Enable Yard Valve 2" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - id: garden_sprinkler_ctrlr + main_switch: "Garden Sprinklers" + auto_advance_switch: "Garden Sprinklers Auto Advance" + reverse_switch: "Garden Sprinklers Reverse" + valve_overlap: 5s + valves: + - valve_switch: "Garden Valve 0" + enable_switch: "Enable Garden Valve 0" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + - valve_switch: "Garden Valve 1" + enable_switch: "Enable Garden Valve 1" + pump_switch_id: gpio_switch1 + run_duration: 10s + valve_switch_id: gpio_switch2 + + cover: - platform: endstop name: Endstop Cover @@ -1378,6 +1438,13 @@ display: on_wake: then: lambda: 'ESP_LOGD("display","Display woke up");' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' + http_request: useragent: esphome/device diff --git a/tests/test4.yaml b/tests/test4.yaml index 0dfbeed550..847639289e 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -80,6 +80,11 @@ sx1509: mcp3204: cs_pin: GPIO23 +dac7678: + address: 0x4A + id: dac7678_hub1 + internal_reference: true + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -518,6 +523,38 @@ output: pipsolar_id: inverter0 battery_recharge_voltage: id: inverter0_battery_recharge_voltage_out + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 0 + id: 'dac7678_1_ch0' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 1 + id: 'dac7678_1_ch1' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 2 + id: 'dac7678_1_ch2' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 3 + id: 'dac7678_1_ch3' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 4 + id: 'dac7678_1_ch4' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 5 + id: 'dac7678_1_ch5' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 6 + id: 'dac7678_1_ch6' + - platform: dac7678 + dac7678_id: 'dac7678_hub1' + channel: 7 + id: 'dac7678_1_ch7' esp32_camera: name: ESP-32 Camera data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19]