mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'beta' into bump-2022.8.0
This commit is contained in:
		
							
								
								
									
										5
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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: | | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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') | ||||
|   | ||||
							
								
								
									
										26
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 }} | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 \ | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -108,6 +108,8 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(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"; | ||||
|   } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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])) | ||||
|   | ||||
| @@ -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<BedjetCodec>(); | ||||
|  | ||||
|   // 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<char const *>(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 | ||||
							
								
								
									
										23
									
								
								esphome/components/bedjet/bedjet_child.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/bedjet/bedjet_child.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<BedJetHub> { | ||||
|  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 | ||||
							
								
								
									
										354
									
								
								esphome/components/bedjet/bedjet_climate.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								esphome/components/bedjet/bedjet_climate.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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 <esp_gattc_api.h> | ||||
| 
 | ||||
| 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::RealTimeClock *> 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<BedjetCodec> 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
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| #include "bedjet_base.h" | ||||
| #include "bedjet_codec.h" | ||||
| #include <cstdio> | ||||
| #include <cstring> | ||||
| 
 | ||||
| @@ -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 <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>", | ||||
|              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 <conn=%c, leds=%c, units=%c, mute=%c; packed=%02x>", | ||||
|               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<const BedjetStatusPacket *>(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
 | ||||
| @@ -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<BedjetStatusPacket> &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<BedjetStatusPacket> status_packet_; | ||||
|   BedjetStatusPacket *status_packet_; | ||||
|   BedjetStatusPacket buf_; | ||||
| }; | ||||
| 
 | ||||
| @@ -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_ \ | ||||
|   | ||||
							
								
								
									
										559
									
								
								esphome/components/bedjet/bedjet_hub.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								esphome/components/bedjet/bedjet_hub.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<char const *>(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 | ||||
							
								
								
									
										178
									
								
								esphome/components/bedjet/bedjet_hub.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								esphome/components/bedjet/bedjet_hub.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <esp_gattc_api.h> | ||||
|  | ||||
| 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<BedjetCodec>(); } | ||||
|   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<BedJetClient *> 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::RealTimeClock *> 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<BedjetCodec> 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 | ||||
| @@ -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])) | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,8 @@ class BinarySensor : public EntityBase { | ||||
|   void add_filter(Filter *filter); | ||||
|   void add_filters(const std::vector<Filter *> &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<std::string> device_class_{};  ///< Stores the override of the device class | ||||
|   Filter *filter_list_{nullptr}; | ||||
|   bool has_state_{false}; | ||||
|   bool publish_initial_state_{false}; | ||||
|   Deduplicator<bool> publish_dedup_; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										75
									
								
								esphome/components/ble_client/automation.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								esphome/components/ble_client/automation.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| #include "automation.h" | ||||
|  | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_gattc_api.h> | ||||
| #include <esp_bt_defs.h> | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ble_client { | ||||
| static const char *const TAG = "ble_client.automation"; | ||||
|  | ||||
| void BLEWriterClientNode::write(const std::vector<uint8_t> &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<uint8_t *>(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 | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
|  | ||||
| #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<uint8_t> &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<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, 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<std::vector<uint8_t>(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const std::vector<uint8_t> &value) { | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   bool has_simple_value_ = true; | ||||
|   std::vector<uint8_t> value_simple_; | ||||
|   std::function<std::vector<uint8_t>(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace ble_client | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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_( | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -1,19 +1,10 @@ | ||||
| #include "climate_traits.h" | ||||
| #include <cstdio> | ||||
|  | ||||
| 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 | ||||
|   | ||||
| @@ -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_)); | ||||
|   | ||||
							
								
								
									
										32
									
								
								esphome/components/dac7678/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/dac7678/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										83
									
								
								esphome/components/dac7678/dac7678_output.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								esphome/components/dac7678/dac7678_output.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<uint16_t>(input_rounded); | ||||
|   this->parent_->set_channel_value_(this->channel_, input); | ||||
| } | ||||
|  | ||||
| }  // namespace dac7678 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										55
									
								
								esphome/components/dac7678/dac7678_output.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								esphome/components/dac7678/dac7678_output.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<DAC7678Output> { | ||||
|  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 | ||||
							
								
								
									
										27
									
								
								esphome/components/dac7678/output.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/dac7678/output.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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])) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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<uint8_t> data); | ||||
|  | ||||
|   uint8_t addr_; | ||||
|  | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_{nullptr}; | ||||
|   sensor::Sensor *battery_level_sensor_{nullptr}; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(); } | ||||
|   | ||||
| @@ -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_; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -4,9 +4,10 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <set> | ||||
| #include <map> | ||||
| #include <vector> | ||||
|  | ||||
| class UDP; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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.") | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -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_); | ||||
|             } | ||||
|           } | ||||
|   | ||||
| @@ -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_); | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/feedback/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/feedback/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@ianchi"] | ||||
							
								
								
									
										157
									
								
								esphome/components/feedback/cover.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								esphome/components/feedback/cover.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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])) | ||||
							
								
								
									
										445
									
								
								esphome/components/feedback/feedback_cover.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								esphome/components/feedback/feedback_cover.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										90
									
								
								esphome/components/feedback/feedback_cover.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								esphome/components/feedback/feedback_cover.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<uint32_t> 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 | ||||
| @@ -44,7 +44,14 @@ template<typename T> 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<typename T> 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_{}; | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -10,14 +10,13 @@ static const char *const TAG = "integration"; | ||||
|  | ||||
| void IntegrationSensor::setup() { | ||||
|   if (this->restore_) { | ||||
|     this->rtc_ = global_preferences->make_preference<float>(this->get_object_id_hash()); | ||||
|     this->pref_ = global_preferences->make_preference<float>(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); }); | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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_(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -185,8 +185,8 @@ inline bool coil_from_vector(int coil, const std::vector<uint8_t> &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<RegisterRange> register_ranges_; | ||||
|   /// Hold the pending requests to be sent | ||||
|   std::list<std::unique_ptr<ModbusCommandItem>> command_queue_; | ||||
|   | ||||
| @@ -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_, | ||||
|   | ||||
| @@ -26,5 +26,12 @@ class WakeTrigger : public Trigger<> { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class PageTrigger : public Trigger<uint8_t> { | ||||
|  public: | ||||
|   explicit PageTrigger(Nextion *nextion) { | ||||
|     nextion->add_new_page_callback([this](const uint8_t page_id) { this->trigger(page_id); }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace nextion | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -152,6 +152,10 @@ void Nextion::add_setup_state_callback(std::function<void()> &&callback) { | ||||
|   this->setup_callback_.add(std::move(callback)); | ||||
| } | ||||
|  | ||||
| void Nextion::add_new_page_callback(std::function<void(uint8_t)> &&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. | ||||
|   | ||||
| @@ -689,6 +689,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | ||||
|    */ | ||||
|   void add_setup_state_callback(std::function<void()> &&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<void(uint8_t)> &&callback); | ||||
|  | ||||
|   void update_all_components(); | ||||
|  | ||||
|   /** | ||||
| @@ -813,6 +819,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | ||||
|   CallbackManager<void()> setup_callback_{}; | ||||
|   CallbackManager<void()> sleep_callback_{}; | ||||
|   CallbackManager<void()> wake_callback_{}; | ||||
|   CallbackManager<void(uint8_t)> page_callback_{}; | ||||
|  | ||||
|   optional<nextion_writer_t> writer_; | ||||
|   float brightness_{1.0}; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ NdefRecord::NdefRecord(std::vector<uint8_t> payload_data) { | ||||
| std::vector<uint8_t> NdefRecord::encode(bool first, bool last) { | ||||
|   std::vector<uint8_t> 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<uint8_t> payload_data = get_encoded_payload(); | ||||
|  | ||||
|   size_t payload_length = payload_data.size(); | ||||
|   | ||||
| @@ -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<bool>(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<bool>(myhash); | ||||
|   bool initial_state; | ||||
|   if (this->rtc_.load(&initial_state)) { | ||||
|     this->previous_in_range_ = initial_state; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
							
								
								
									
										24
									
								
								esphome/components/pulse_counter/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								esphome/components/pulse_counter/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> { | ||||
|  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 | ||||
| @@ -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_); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data); | ||||
|   bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result); | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user