mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into bump-2022.5.0b1
This commit is contained in:
		| @@ -28,8 +28,10 @@ esphome/components/atc_mithermometer/* @ahpohl | |||||||
| esphome/components/b_parasite/* @rbaron | esphome/components/b_parasite/* @rbaron | ||||||
| esphome/components/ballu/* @bazuchan | esphome/components/ballu/* @bazuchan | ||||||
| esphome/components/bang_bang/* @OttoWinter | esphome/components/bang_bang/* @OttoWinter | ||||||
|  | esphome/components/bedjet/* @jhansche | ||||||
| esphome/components/bh1750/* @OttoWinter | esphome/components/bh1750/* @OttoWinter | ||||||
| esphome/components/binary_sensor/* @esphome/core | esphome/components/binary_sensor/* @esphome/core | ||||||
|  | esphome/components/bl0939/* @ziceva | ||||||
| esphome/components/bl0940/* @tobias- | esphome/components/bl0940/* @tobias- | ||||||
| esphome/components/ble_client/* @buxtronix | esphome/components/ble_client/* @buxtronix | ||||||
| esphome/components/bme680_bsec/* @trvrnrth | esphome/components/bme680_bsec/* @trvrnrth | ||||||
| @@ -53,11 +55,13 @@ esphome/components/current_based/* @djwmarcx | |||||||
| esphome/components/daly_bms/* @s1lvi0 | esphome/components/daly_bms/* @s1lvi0 | ||||||
| esphome/components/dashboard_import/* @esphome/core | esphome/components/dashboard_import/* @esphome/core | ||||||
| esphome/components/debug/* @OttoWinter | esphome/components/debug/* @OttoWinter | ||||||
|  | esphome/components/delonghi/* @grob6000 | ||||||
| esphome/components/dfplayer/* @glmnet | esphome/components/dfplayer/* @glmnet | ||||||
| esphome/components/dht/* @OttoWinter | esphome/components/dht/* @OttoWinter | ||||||
| esphome/components/ds1307/* @badbadc0ffee | esphome/components/ds1307/* @badbadc0ffee | ||||||
| esphome/components/dsmr/* @glmnet @zuidwijk | esphome/components/dsmr/* @glmnet @zuidwijk | ||||||
| esphome/components/ektf2232/* @jesserockz | esphome/components/ektf2232/* @jesserockz | ||||||
|  | esphome/components/ens210/* @itn3rd77 | ||||||
| esphome/components/esp32/* @esphome/core | esphome/components/esp32/* @esphome/core | ||||||
| esphome/components/esp32_ble/* @jesserockz | esphome/components/esp32_ble/* @jesserockz | ||||||
| esphome/components/esp32_ble_server/* @jesserockz | esphome/components/esp32_ble_server/* @jesserockz | ||||||
| @@ -164,12 +168,13 @@ esphome/components/rf_bridge/* @jesserockz | |||||||
| esphome/components/rgbct/* @jesserockz | esphome/components/rgbct/* @jesserockz | ||||||
| esphome/components/rtttl/* @glmnet | esphome/components/rtttl/* @glmnet | ||||||
| esphome/components/safe_mode/* @jsuanet @paulmonigatti | esphome/components/safe_mode/* @jsuanet @paulmonigatti | ||||||
| esphome/components/scd4x/* @sjtrny | esphome/components/scd4x/* @martgras @sjtrny | ||||||
| esphome/components/script/* @esphome/core | esphome/components/script/* @esphome/core | ||||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||||
| esphome/components/sdp3x/* @Azimath | esphome/components/sdp3x/* @Azimath | ||||||
| esphome/components/selec_meter/* @sourabhjaiswal | esphome/components/selec_meter/* @sourabhjaiswal | ||||||
| esphome/components/select/* @esphome/core | esphome/components/select/* @esphome/core | ||||||
|  | esphome/components/sen5x/* @martgras | ||||||
| esphome/components/sensirion_common/* @martgras | esphome/components/sensirion_common/* @martgras | ||||||
| esphome/components/sensor/* @esphome/core | esphome/components/sensor/* @esphome/core | ||||||
| esphome/components/sgp40/* @SenexCrenshaw | esphome/components/sgp40/* @SenexCrenshaw | ||||||
| @@ -178,9 +183,11 @@ esphome/components/sht4x/* @sjtrny | |||||||
| esphome/components/shutdown/* @esphome/core @jsuanet | esphome/components/shutdown/* @esphome/core @jsuanet | ||||||
| esphome/components/sim800l/* @glmnet | esphome/components/sim800l/* @glmnet | ||||||
| esphome/components/sm2135/* @BoukeHaarsma23 | esphome/components/sm2135/* @BoukeHaarsma23 | ||||||
|  | esphome/components/sml/* @alengwenus | ||||||
| esphome/components/socket/* @esphome/core | esphome/components/socket/* @esphome/core | ||||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||||
| esphome/components/spi/* @esphome/core | esphome/components/spi/* @esphome/core | ||||||
|  | esphome/components/sps30/* @martgras | ||||||
| esphome/components/ssd1322_base/* @kbx81 | esphome/components/ssd1322_base/* @kbx81 | ||||||
| esphome/components/ssd1322_spi/* @kbx81 | esphome/components/ssd1322_spi/* @kbx81 | ||||||
| esphome/components/ssd1325_base/* @kbx81 | esphome/components/ssd1325_base/* @kbx81 | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ RUN \ | |||||||
|         iputils-ping=3:20210202-1 \ |         iputils-ping=3:20210202-1 \ | ||||||
|         git=1:2.30.2-1 \ |         git=1:2.30.2-1 \ | ||||||
|         curl=7.74.0-1.3+deb11u1 \ |         curl=7.74.0-1.3+deb11u1 \ | ||||||
|  |         openssh-client=1:8.4p1-5 \ | ||||||
|     && rm -rf \ |     && rm -rf \ | ||||||
|         /tmp/* \ |         /tmp/* \ | ||||||
|         /var/{cache,log}/* \ |         /var/{cache,log}/* \ | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import argparse | |||||||
| import functools | import functools | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
|  | import re | ||||||
| import sys | import sys | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| @@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.config import iter_components, read_config, strip_default_ids | from esphome.config import iter_components, read_config, strip_default_ids | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|  |     ALLOWED_NAME_CHARS, | ||||||
|     CONF_BAUD_RATE, |     CONF_BAUD_RATE, | ||||||
|     CONF_BROKER, |     CONF_BROKER, | ||||||
|     CONF_DEASSERT_RTS_DTR, |     CONF_DEASSERT_RTS_DTR, | ||||||
|     CONF_LOGGER, |     CONF_LOGGER, | ||||||
|  |     CONF_NAME, | ||||||
|     CONF_OTA, |     CONF_OTA, | ||||||
|     CONF_PASSWORD, |     CONF_PASSWORD, | ||||||
|     CONF_PORT, |     CONF_PORT, | ||||||
|     CONF_ESPHOME, |     CONF_ESPHOME, | ||||||
|     CONF_PLATFORMIO_OPTIONS, |     CONF_PLATFORMIO_OPTIONS, | ||||||
|  |     CONF_SUBSTITUTIONS, | ||||||
|     SECRETS_FILES, |     SECRETS_FILES, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, EsphomeError, coroutine | from esphome.core import CORE, EsphomeError, coroutine | ||||||
| @@ -481,6 +485,98 @@ def command_idedata(args, config): | |||||||
|     return 0 |     return 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def command_rename(args, config): | ||||||
|  |     for c in args.name: | ||||||
|  |         if c not in ALLOWED_NAME_CHARS: | ||||||
|  |             print( | ||||||
|  |                 color( | ||||||
|  |                     Fore.BOLD_RED, | ||||||
|  |                     f"'{c}' is an invalid character for names. Valid characters are: " | ||||||
|  |                     f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             return 1 | ||||||
|  |     # Load existing yaml file | ||||||
|  |     with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: | ||||||
|  |         raw_contents = raw_file.read() | ||||||
|  |  | ||||||
|  |     yaml = yaml_util.load_yaml(CORE.config_path) | ||||||
|  |     if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: | ||||||
|  |         print( | ||||||
|  |             color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") | ||||||
|  |         ) | ||||||
|  |         return 1 | ||||||
|  |     old_name = yaml[CONF_ESPHOME][CONF_NAME] | ||||||
|  |     match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) | ||||||
|  |     if match is None: | ||||||
|  |         new_raw = re.sub( | ||||||
|  |             rf"name:\s+[\"']?{old_name}[\"']?", | ||||||
|  |             f'name: "{args.name}"', | ||||||
|  |             raw_contents, | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] | ||||||
|  |         if ( | ||||||
|  |             len( | ||||||
|  |                 re.findall( | ||||||
|  |                     rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", | ||||||
|  |                     raw_contents, | ||||||
|  |                     flags=re.MULTILINE, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             > 1 | ||||||
|  |         ): | ||||||
|  |             print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) | ||||||
|  |             return 1 | ||||||
|  |  | ||||||
|  |         new_raw = re.sub( | ||||||
|  |             rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", | ||||||
|  |             f'\\1: "{args.name}"', | ||||||
|  |             raw_contents, | ||||||
|  |             flags=re.MULTILINE, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     new_path = os.path.join(CORE.config_dir, args.name + ".yaml") | ||||||
|  |     print( | ||||||
|  |         f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" | ||||||
|  |     ) | ||||||
|  |     print() | ||||||
|  |  | ||||||
|  |     with open(new_path, mode="w", encoding="utf-8") as new_file: | ||||||
|  |         new_file.write(new_raw) | ||||||
|  |  | ||||||
|  |     rc = run_external_process("esphome", "config", new_path) | ||||||
|  |     if rc != 0: | ||||||
|  |         print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) | ||||||
|  |         os.remove(new_path) | ||||||
|  |         return 1 | ||||||
|  |  | ||||||
|  |     cli_args = [ | ||||||
|  |         "run", | ||||||
|  |         new_path, | ||||||
|  |         "--no-logs", | ||||||
|  |         "--device", | ||||||
|  |         CORE.address, | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     if args.dashboard: | ||||||
|  |         cli_args.insert(0, "--dashboard") | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         rc = run_external_process("esphome", *cli_args) | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         rc = 1 | ||||||
|  |     if rc != 0: | ||||||
|  |         os.remove(new_path) | ||||||
|  |         return 1 | ||||||
|  |  | ||||||
|  |     os.remove(CORE.config_path) | ||||||
|  |  | ||||||
|  |     print(color(Fore.BOLD_GREEN, "SUCCESS")) | ||||||
|  |     print() | ||||||
|  |     return 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| PRE_CONFIG_ACTIONS = { | PRE_CONFIG_ACTIONS = { | ||||||
|     "wizard": command_wizard, |     "wizard": command_wizard, | ||||||
|     "version": command_version, |     "version": command_version, | ||||||
| @@ -499,6 +595,7 @@ POST_CONFIG_ACTIONS = { | |||||||
|     "mqtt-fingerprint": command_mqtt_fingerprint, |     "mqtt-fingerprint": command_mqtt_fingerprint, | ||||||
|     "clean": command_clean, |     "clean": command_clean, | ||||||
|     "idedata": command_idedata, |     "idedata": command_idedata, | ||||||
|  |     "rename": command_rename, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -681,6 +778,15 @@ def parse_args(argv): | |||||||
|         "configuration", help="Your YAML configuration file(s).", nargs=1 |         "configuration", help="Your YAML configuration file(s).", nargs=1 | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     parser_rename = subparsers.add_parser( | ||||||
|  |         "rename", | ||||||
|  |         help="Rename a device in YAML, compile the binary and upload it.", | ||||||
|  |     ) | ||||||
|  |     parser_rename.add_argument( | ||||||
|  |         "configuration", help="Your YAML configuration file.", nargs=1 | ||||||
|  |     ) | ||||||
|  |     parser_rename.add_argument("name", help="The new name for the device.", type=str) | ||||||
|  |  | ||||||
|     # Keep backward compatibility with the old command line format of |     # Keep backward compatibility with the old command line format of | ||||||
|     # esphome <config> <command>. |     # esphome <config> <command>. | ||||||
|     # |     # | ||||||
|   | |||||||
| @@ -64,6 +64,7 @@ from esphome.cpp_types import (  # noqa | |||||||
|     uint64, |     uint64, | ||||||
|     int32, |     int32, | ||||||
|     int64, |     int64, | ||||||
|  |     size_t, | ||||||
|     const_char_ptr, |     const_char_ptr, | ||||||
|     NAN, |     NAN, | ||||||
|     esphome_ns, |     esphome_ns, | ||||||
|   | |||||||
| @@ -51,8 +51,8 @@ void ADCSensor::setup() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 |   // 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) | #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_); |   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); | ||||||
| #endif | #endif | ||||||
| #endif  // USE_ESP32 | #endif  // USE_ESP32 | ||||||
|   | |||||||
| @@ -94,6 +94,29 @@ async def to_code(config): | |||||||
|                 data[pos] = pix[2] |                 data[pos] = pix[2] | ||||||
|                 pos += 1 |                 pos += 1 | ||||||
|  |  | ||||||
|  |     elif config[CONF_TYPE] == "RGB565": | ||||||
|  |         data = [0 for _ in range(height * width * 2 * frames)] | ||||||
|  |         pos = 0 | ||||||
|  |         for frameIndex in range(frames): | ||||||
|  |             image.seek(frameIndex) | ||||||
|  |             frame = image.convert("RGB") | ||||||
|  |             if CONF_RESIZE in config: | ||||||
|  |                 frame = frame.resize([width, height]) | ||||||
|  |             pixels = list(frame.getdata()) | ||||||
|  |             if len(pixels) != height * width: | ||||||
|  |                 raise core.EsphomeError( | ||||||
|  |                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" | ||||||
|  |                 ) | ||||||
|  |             for pix in pixels: | ||||||
|  |                 R = pix[0] >> 3 | ||||||
|  |                 G = pix[1] >> 2 | ||||||
|  |                 B = pix[2] >> 3 | ||||||
|  |                 rgb = (R << 11) | (G << 5) | B | ||||||
|  |                 data[pos] = rgb >> 8 | ||||||
|  |                 pos += 1 | ||||||
|  |                 data[pos] = rgb & 255 | ||||||
|  |                 pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] == "BINARY": |     elif config[CONF_TYPE] == "BINARY": | ||||||
|         width8 = ((width + 7) // 8) * 8 |         width8 = ((width + 7) // 8) * 8 | ||||||
|         data = [0 for _ in range((height * width8 // 8) * frames)] |         data = [0 for _ in range((height * width8 // 8) * frames)] | ||||||
|   | |||||||
| @@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
| void APIServer::on_select_update(select::Select *obj, const std::string &state) { | void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { | ||||||
|   if (obj->is_internal()) |   if (obj->is_internal()) | ||||||
|     return; |     return; | ||||||
|   for (auto &c : this->clients_) |   for (auto &c : this->clients_) | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ class APIServer : public Component, public Controller { | |||||||
|   void on_number_update(number::Number *obj, float state) override; |   void on_number_update(number::Number *obj, float state) override; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   void on_select_update(select::Select *obj, const std::string &state) override; |   void on_select_update(select::Select *obj, const std::string &state, size_t index) override; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_LOCK | #ifdef USE_LOCK | ||||||
|   void on_lock_update(lock::Lock *obj) override; |   void on_lock_update(lock::Lock *obj) override; | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@jhansche"] | ||||||
							
								
								
									
										642
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										642
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,642 @@ | |||||||
|  | #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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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(BTN_EXTHT); | ||||||
|  |         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 & 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 { | ||||||
|  |       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->node_state != espbt::ClientState::ESTABLISHED) { | ||||||
|  |     ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   auto *time_id = *this->time_id_; | ||||||
|  |   time::ESPTime now = time_id->now(); | ||||||
|  |   if (now.is_valid()) { | ||||||
|  |     uint8_t hour = now.hour; | ||||||
|  |     uint8_t minute = now.minute; | ||||||
|  |     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); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** 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_(); }); | ||||||
|  |     time::ESPTime now = time_id->now(); | ||||||
|  |     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."); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | /** 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: | ||||||
|  |     case MODE_EXTHT: | ||||||
|  |       this->mode = climate::CLIMATE_MODE_HEAT; | ||||||
|  |       this->action = climate::CLIMATE_ACTION_HEATING; | ||||||
|  |       this->custom_preset.reset(); | ||||||
|  |       this->preset.reset(); | ||||||
|  |       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 | ||||||
							
								
								
									
										121
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | #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/hal.h" | ||||||
|  | #include "bedjet_base.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; | ||||||
|  |  | ||||||
|  | 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 { | ||||||
|  |  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; } | ||||||
|  | #endif | ||||||
|  |   void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } | ||||||
|  |  | ||||||
|  |   /** Attempts to check for and apply firmware updates. */ | ||||||
|  |   void upgrade_firmware(); | ||||||
|  |  | ||||||
|  |   climate::ClimateTraits traits() override { | ||||||
|  |     auto traits = climate::ClimateTraits(); | ||||||
|  |     traits.set_supports_action(true); | ||||||
|  |     traits.set_supports_current_temperature(true); | ||||||
|  |     traits.set_supported_modes({ | ||||||
|  |         climate::CLIMATE_MODE_OFF, | ||||||
|  |         climate::CLIMATE_MODE_HEAT, | ||||||
|  |         // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead | ||||||
|  |         climate::CLIMATE_MODE_FAN_ONLY, | ||||||
|  |         climate::CLIMATE_MODE_DRY, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // It would be better if we had a slider for the fan modes. | ||||||
|  |     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); | ||||||
|  |     traits.set_supported_presets({ | ||||||
|  |         // If we support NONE, then have to decide what happens if the user switches to it (turn off?) | ||||||
|  |         // climate::CLIMATE_PRESET_NONE, | ||||||
|  |         // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. | ||||||
|  |         climate::CLIMATE_PRESET_BOOST, | ||||||
|  |     }); | ||||||
|  |     traits.set_supported_custom_presets({ | ||||||
|  |         // We could fetch biodata from bedjet and set these names that way. | ||||||
|  |         // But then we have to invert the lookup in order to send the right preset. | ||||||
|  |         // For now, we can leave them as M1-3 to match the remote buttons. | ||||||
|  |         "M1", | ||||||
|  |         "M2", | ||||||
|  |         "M3", | ||||||
|  |     }); | ||||||
|  |     traits.set_visual_min_temperature(19.0); | ||||||
|  |     traits.set_visual_max_temperature(43.0); | ||||||
|  |     traits.set_visual_temperature_step(1.0); | ||||||
|  |     return traits; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void control(const climate::ClimateCall &call) override; | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  |   void setup_time_(); | ||||||
|  |   void send_local_time_(); | ||||||
|  |   optional<time::RealTimeClock *> time_id_{}; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; | ||||||
|  |  | ||||||
|  |   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_(); | ||||||
|  |  | ||||||
|  |   bool is_valid_() { | ||||||
|  |     // FIXME: find a better way to check this? | ||||||
|  |     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 | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | #include "bedjet_base.h" | ||||||
|  | #include <cstdio> | ||||||
|  | #include <cstring> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | /// Converts a BedJet temp step into degrees Fahrenheit. | ||||||
|  | float bedjet_temp_to_f(const uint8_t temp) { | ||||||
|  |   // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32. | ||||||
|  |   return 0.9f * temp + 32.0f; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Cleans up the packet before sending. */ | ||||||
|  | BedjetPacket *BedjetCodec::clean_packet_() { | ||||||
|  |   // So far no commands require more than 2 bytes of data. | ||||||
|  |   assert(this->packet_.data_length <= 2); | ||||||
|  |   for (int i = this->packet_.data_length; i < 2; i++) { | ||||||
|  |     this->packet_.data[i] = '\0'; | ||||||
|  |   } | ||||||
|  |   ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]); | ||||||
|  |   return &this->packet_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will initiate a BedjetButton press. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) { | ||||||
|  |   this->packet_.command = CMD_BUTTON; | ||||||
|  |   this->packet_.data_length = 1; | ||||||
|  |   this->packet_.data[0] = button; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will set the device's target `temperature`. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) { | ||||||
|  |   this->packet_.command = CMD_SET_TEMP; | ||||||
|  |   this->packet_.data_length = 1; | ||||||
|  |   this->packet_.data[0] = temperature * 2; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Returns a BedjetPacket that will set the device's target fan speed. */ | ||||||
|  | BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { | ||||||
|  |   this->packet_.command = CMD_SET_FAN; | ||||||
|  |   this->packet_.data_length = 1; | ||||||
|  |   this->packet_.data[0] = fan_step; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** 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_.data_length = 2; | ||||||
|  |   this->packet_.data[0] = hour; | ||||||
|  |   this->packet_.data[1] = minute; | ||||||
|  |   return this->clean_packet_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** 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]); | ||||||
|  |   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)); | ||||||
|  |   } 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); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Decodes the incoming status packet received on the BEDJET_STATUS_UUID. | ||||||
|  |  * | ||||||
|  |  * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. | ||||||
|  |  */ | ||||||
|  | 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 | ||||||
|  |     memcpy(&this->buf_, data, 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) { | ||||||
|  |       // and save it for the update() loop | ||||||
|  |       this->status_packet_ = this->buf_; | ||||||
|  |       return this->buf_.is_partial == 1; | ||||||
|  |     } else { | ||||||
|  |       // 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]); | ||||||
|  |  | ||||||
|  |     if (this->has_status()) { | ||||||
|  |       this->status_packet_->ambient_temp_step = data[6]; | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // TODO: log a warning if we detect that we connected to a non-V3 device. | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #include "bedjet_const.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | struct BedjetPacket { | ||||||
|  |   uint8_t data_length; | ||||||
|  |   BedjetCommand command; | ||||||
|  |   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 | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BedjetPacketType : uint8_t { | ||||||
|  |   PACKET_TYPE_STATUS = 0x1, | ||||||
|  |   PACKET_TYPE_DEBUG = 0x2, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** 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. | ||||||
|  |   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. | ||||||
|  |   BedjetPacketType packet_type : 8;  ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet. | ||||||
|  |  | ||||||
|  |   // [4] | ||||||
|  |   uint8_t time_remaining_hrs : 8;   ///< Hours remaining in program runtime | ||||||
|  |   uint8_t time_remaining_mins : 8;  ///< Minutes remaining in program runtime | ||||||
|  |   uint8_t time_remaining_secs : 8;  ///< Seconds remaining in program runtime | ||||||
|  |  | ||||||
|  |   // [7] | ||||||
|  |   uint8_t actual_temp_step : 8;  ///< Actual temp of the air blown by the BedJet fan; value represents `2 * | ||||||
|  |                                  ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f | ||||||
|  |   uint8_t target_temp_step : 8;  ///< Target temp that the BedJet will try to heat to. See #actual_temp_step. | ||||||
|  |  | ||||||
|  |   // [9] | ||||||
|  |   BedjetMode mode : 8;  ///< BedJet operating mode. | ||||||
|  |  | ||||||
|  |   // [10] | ||||||
|  |   uint8_t fan_step : 8;  ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5 | ||||||
|  |                          ///< * fan_step` | ||||||
|  |   uint8_t max_hrs : 8;   ///< Max hours of mode runtime | ||||||
|  |   uint8_t max_mins : 8;  ///< Max minutes of mode runtime | ||||||
|  |   uint8_t min_temp_step : 8;  ///< Min temp allowed in mode. See #actual_temp_step. | ||||||
|  |   uint8_t max_temp_step : 8;  ///< Max temp allowed in mode. See #actual_temp_step. | ||||||
|  |  | ||||||
|  |   // [15-16] | ||||||
|  |   uint16_t turbo_time : 16;  ///< Time remaining in BedjetMode::MODE_TURBO. | ||||||
|  |  | ||||||
|  |   // [17] | ||||||
|  |   uint8_t ambient_temp_step : 8;  ///< Current ambient air temp. This is the coldest air the BedJet can blow. See | ||||||
|  |                                   ///< #actual_temp_step. | ||||||
|  |   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 | ||||||
|  |  | ||||||
|  |   // [26] | ||||||
|  |   //   0x18(24) = "Connection test has completed OK" | ||||||
|  |   //   0x1a(26) = "Firmware update is not needed" | ||||||
|  |   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 | ||||||
|  |  | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | /** This class is responsible for encoding command packets and decoding status packets. | ||||||
|  |  * | ||||||
|  |  * Status Packets | ||||||
|  |  * ============== | ||||||
|  |  * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID | ||||||
|  |  * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off, | ||||||
|  |  * it generally will not notify of any status. | ||||||
|  |  * | ||||||
|  |  * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets, | ||||||
|  |  * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional | ||||||
|  |  * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the | ||||||
|  |  * full status packet. | ||||||
|  |  * | ||||||
|  |  * Command Packets | ||||||
|  |  * =============== | ||||||
|  |  * This class supports encoding a number of BedjetPacket commands: | ||||||
|  |  * - Button press | ||||||
|  |  *   This simulates a press of one of the BedjetButton values. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_BUTTON | ||||||
|  |  *   - BedjetPacket#data [0] contains the BedjetButton value | ||||||
|  |  * - Set target temp | ||||||
|  |  *   This sets the BedJet's target temp to a concrete temperature value. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP | ||||||
|  |  *   - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step | ||||||
|  |  * - Set fan speed | ||||||
|  |  *   This sets the BedJet fan speed. | ||||||
|  |  *   - BedjetPacket#command = BedjetCommand::CMD_SET_FAN | ||||||
|  |  *   - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19. | ||||||
|  |  * - 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#data [0] is hours, [1] is minutes | ||||||
|  |  */ | ||||||
|  | class BedjetCodec { | ||||||
|  |  public: | ||||||
|  |   BedjetPacket *get_button_request(BedjetButton button); | ||||||
|  |   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); | ||||||
|  |  | ||||||
|  |   bool decode_notify(const uint8_t *data, uint16_t length); | ||||||
|  |   void decode_extra(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(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   BedjetPacket *clean_packet_(); | ||||||
|  |  | ||||||
|  |   uint8_t last_buffer_size_ = 0; | ||||||
|  |  | ||||||
|  |   BedjetPacket packet_; | ||||||
|  |  | ||||||
|  |   optional<BedjetStatusPacket> status_packet_; | ||||||
|  |   BedjetStatusPacket buf_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <set> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bedjet { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "bedjet"; | ||||||
|  |  | ||||||
|  | enum BedjetMode : uint8_t { | ||||||
|  |   /// BedJet is Off | ||||||
|  |   MODE_STANDBY = 0, | ||||||
|  |   /// BedJet is in Heat mode (limited to 4 hours) | ||||||
|  |   MODE_HEAT = 1, | ||||||
|  |   /// BedJet is in Turbo mode (high heat, limited time) | ||||||
|  |   MODE_TURBO = 2, | ||||||
|  |   /// BedJet is in Extended Heat mode (limited to 10 hours) | ||||||
|  |   MODE_EXTHT = 3, | ||||||
|  |   /// BedJet is in Cool mode (actually "Fan only" mode) | ||||||
|  |   MODE_COOL = 4, | ||||||
|  |   /// BedJet is in Dry mode (high speed, no heat) | ||||||
|  |   MODE_DRY = 5, | ||||||
|  |   /// BedJet is in "wait" mode, a step during a biorhythm program | ||||||
|  |   MODE_WAIT = 6, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BedjetButton : uint8_t { | ||||||
|  |   /// Turn BedJet off | ||||||
|  |   BTN_OFF = 0x1, | ||||||
|  |   /// Enter Cool mode (fan only) | ||||||
|  |   BTN_COOL = 0x2, | ||||||
|  |   /// Enter Heat mode (limited to 4 hours) | ||||||
|  |   BTN_HEAT = 0x3, | ||||||
|  |   /// Enter Turbo mode (high heat, limited to 10 minutes) | ||||||
|  |   BTN_TURBO = 0x4, | ||||||
|  |   /// Enter Dry mode (high speed, no heat) | ||||||
|  |   BTN_DRY = 0x5, | ||||||
|  |   /// Enter Extended Heat mode (limited to 10 hours) | ||||||
|  |   BTN_EXTHT = 0x6, | ||||||
|  |  | ||||||
|  |   /// Start the M1 biorhythm/preset program | ||||||
|  |   BTN_M1 = 0x20, | ||||||
|  |   /// Start the M2 biorhythm/preset program | ||||||
|  |   BTN_M2 = 0x21, | ||||||
|  |   /// Start the M3 biorhythm/preset program | ||||||
|  |   BTN_M3 = 0x22, | ||||||
|  |  | ||||||
|  |   /* These are "MAGIC" buttons */ | ||||||
|  |  | ||||||
|  |   /// Turn debug mode on/off | ||||||
|  |   MAGIC_DEBUG_ON = 0x40, | ||||||
|  |   MAGIC_DEBUG_OFF = 0x41, | ||||||
|  |   /// Perform a connection test. | ||||||
|  |   MAGIC_CONNTEST = 0x42, | ||||||
|  |   /// Request a firmware update. This will also restart the Bedjet. | ||||||
|  |   MAGIC_UPDATE = 0x43, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BedjetCommand : uint8_t { | ||||||
|  |   CMD_BUTTON = 0x1, | ||||||
|  |   CMD_SET_TEMP = 0x3, | ||||||
|  |   CMD_STATUS = 0x6, | ||||||
|  |   CMD_SET_FAN = 0x7, | ||||||
|  |   CMD_SET_TIME = 0x8, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #define BEDJET_FAN_STEP_NAMES_ \ | ||||||
|  |   { \ | ||||||
|  |     "  5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ | ||||||
|  |         " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; | ||||||
|  | static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_; | ||||||
|  | static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||||
|  |  | ||||||
|  | }  // namespace bedjet | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import climate, ble_client, time | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_RECEIVE_TIMEOUT, | ||||||
|  |     CONF_TIME_ID, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@jhansche"] | ||||||
|  | DEPENDENCIES = ["ble_client"] | ||||||
|  |  | ||||||
|  | bedjet_ns = cg.esphome_ns.namespace("bedjet") | ||||||
|  | Bedjet = bedjet_ns.class_( | ||||||
|  |     "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     climate.CLIMATE_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(Bedjet), | ||||||
|  |             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")) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  |     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
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@ziceva"] | ||||||
							
								
								
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | |||||||
|  | #include "bl0939.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | 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) | ||||||
|  | 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; | ||||||
|  |  | ||||||
|  | static const uint8_t BL0939_WRITE_COMMAND = 0xA5;  // 0xA{A4,A3,A2,A1} | ||||||
|  | static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10; | ||||||
|  | static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E; | ||||||
|  | static const uint8_t BL0939_REG_MODE = 0x18; | ||||||
|  | static const uint8_t BL0939_REG_SOFT_RESET = 0x19; | ||||||
|  | static const uint8_t BL0939_REG_USR_WRPROT = 0x1A; | ||||||
|  | static const uint8_t BL0939_REG_TPS_CTRL = 0x1B; | ||||||
|  |  | ||||||
|  | const uint8_t BL0939_INIT[6][6] = { | ||||||
|  |     // Reset to default | ||||||
|  |     {BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33}, | ||||||
|  |     // Enable User Operation Write | ||||||
|  |     {BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB}, | ||||||
|  |     // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS | ||||||
|  |     {BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32}, | ||||||
|  |     // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS | ||||||
|  |     {BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9}, | ||||||
|  |     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||||
|  |     {BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16}, | ||||||
|  |     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||||
|  |     {BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}}; | ||||||
|  |  | ||||||
|  | void BL0939::loop() { | ||||||
|  |   DataPacket buffer; | ||||||
|  |   if (!this->available()) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||||
|  |     if (validate_checksum(&buffer)) { | ||||||
|  |       received_package_(&buffer); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||||
|  |     while (read() >= 0) | ||||||
|  |       ; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool BL0939::validate_checksum(const DataPacket *data) { | ||||||
|  |   uint8_t checksum = BL0939_READ_COMMAND; | ||||||
|  |   // Whole package but checksum | ||||||
|  |   for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { | ||||||
|  |     checksum += data->raw[i]; | ||||||
|  |   } | ||||||
|  |   checksum ^= 0xFF; | ||||||
|  |   if (checksum != data->checksum) { | ||||||
|  |     ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); | ||||||
|  |   } | ||||||
|  |   return checksum == data->checksum; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BL0939::update() { | ||||||
|  |   this->flush(); | ||||||
|  |   this->write_byte(BL0939_READ_COMMAND); | ||||||
|  |   this->write_byte(BL0939_FULL_PACKET); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BL0939::setup() { | ||||||
|  |   for (auto *i : BL0939_INIT) { | ||||||
|  |     this->write_array(i, 6); | ||||||
|  |     delay(1); | ||||||
|  |   } | ||||||
|  |   this->flush(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BL0939::received_package_(const DataPacket *data) const { | ||||||
|  |   // Bad header | ||||||
|  |   if (data->frame_header != BL0939_PACKET_HEADER) { | ||||||
|  |     ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; | ||||||
|  |   float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_; | ||||||
|  |   float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_; | ||||||
|  |   float a_watt = (float) to_int32_t(data->a_watt) / power_reference_; | ||||||
|  |   float b_watt = (float) to_int32_t(data->b_watt) / power_reference_; | ||||||
|  |   int32_t cfa_cnt = to_int32_t(data->cfa_cnt); | ||||||
|  |   int32_t cfb_cnt = to_int32_t(data->cfb_cnt); | ||||||
|  |   float a_energy_consumption = (float) cfa_cnt / energy_reference_; | ||||||
|  |   float b_energy_consumption = (float) cfb_cnt / energy_reference_; | ||||||
|  |   float total_energy_consumption = a_energy_consumption + b_energy_consumption; | ||||||
|  |  | ||||||
|  |   if (voltage_sensor_ != nullptr) { | ||||||
|  |     voltage_sensor_->publish_state(v_rms); | ||||||
|  |   } | ||||||
|  |   if (current_sensor_1_ != nullptr) { | ||||||
|  |     current_sensor_1_->publish_state(ia_rms); | ||||||
|  |   } | ||||||
|  |   if (current_sensor_2_ != nullptr) { | ||||||
|  |     current_sensor_2_->publish_state(ib_rms); | ||||||
|  |   } | ||||||
|  |   if (power_sensor_1_ != nullptr) { | ||||||
|  |     power_sensor_1_->publish_state(a_watt); | ||||||
|  |   } | ||||||
|  |   if (power_sensor_2_ != nullptr) { | ||||||
|  |     power_sensor_2_->publish_state(b_watt); | ||||||
|  |   } | ||||||
|  |   if (energy_sensor_1_ != nullptr) { | ||||||
|  |     energy_sensor_1_->publish_state(a_energy_consumption); | ||||||
|  |   } | ||||||
|  |   if (energy_sensor_2_ != nullptr) { | ||||||
|  |     energy_sensor_2_->publish_state(b_energy_consumption); | ||||||
|  |   } | ||||||
|  |   if (energy_sensor_sum_ != nullptr) { | ||||||
|  |     energy_sensor_sum_->publish_state(total_energy_consumption); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms, | ||||||
|  |            ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BL0939::dump_config() {  // NOLINT(readability-function-cognitive-complexity) | ||||||
|  |   ESP_LOGCONFIG(TAG, "BL0939:"); | ||||||
|  |   LOG_SENSOR("", "Voltage", this->voltage_sensor_); | ||||||
|  |   LOG_SENSOR("", "Current 1", this->current_sensor_1_); | ||||||
|  |   LOG_SENSOR("", "Current 2", this->current_sensor_2_); | ||||||
|  |   LOG_SENSOR("", "Power 1", this->power_sensor_1_); | ||||||
|  |   LOG_SENSOR("", "Power 2", this->power_sensor_2_); | ||||||
|  |   LOG_SENSOR("", "Energy 1", this->energy_sensor_1_); | ||||||
|  |   LOG_SENSOR("", "Energy 2", this->energy_sensor_2_); | ||||||
|  |   LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } | ||||||
|  |  | ||||||
|  | int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } | ||||||
|  |  | ||||||
|  | }  // namespace bl0939 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  | // 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) | ||||||
|  | static const float BL0939_IREF = 324004 * 1 / 1.218; | ||||||
|  | static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51)); | ||||||
|  | static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51)); | ||||||
|  | static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51)); | ||||||
|  |  | ||||||
|  | struct ube24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||||
|  |   uint8_t l; | ||||||
|  |   uint8_t m; | ||||||
|  |   uint8_t h; | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | struct ube16_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||||
|  |   uint8_t l; | ||||||
|  |   uint8_t h; | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | struct sbe24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||||
|  |   uint8_t l; | ||||||
|  |   uint8_t m; | ||||||
|  |   int8_t h; | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | // Caveat: All these values are big endian (low - middle - high) | ||||||
|  |  | ||||||
|  | union DataPacket {  // NOLINT(altera-struct-pack-align) | ||||||
|  |   uint8_t raw[35]; | ||||||
|  |   struct { | ||||||
|  |     uint8_t frame_header;  // 0x55 according to docs | ||||||
|  |     ube24_t ia_fast_rms; | ||||||
|  |     ube24_t ia_rms; | ||||||
|  |     ube24_t ib_rms; | ||||||
|  |     ube24_t v_rms; | ||||||
|  |     ube24_t ib_fast_rms; | ||||||
|  |     sbe24_t a_watt; | ||||||
|  |     sbe24_t b_watt; | ||||||
|  |     sbe24_t cfa_cnt; | ||||||
|  |     sbe24_t cfb_cnt; | ||||||
|  |     ube16_t tps1; | ||||||
|  |     uint8_t RESERVED1;  // value of 0x00 | ||||||
|  |     ube16_t tps2; | ||||||
|  |     uint8_t RESERVED2;  // value of 0x00 | ||||||
|  |     uint8_t checksum;   // checksum | ||||||
|  |   }; | ||||||
|  | } __attribute__((packed)); | ||||||
|  |  | ||||||
|  | class BL0939 : public PollingComponent, public uart::UARTDevice { | ||||||
|  |  public: | ||||||
|  |   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||||
|  |   void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } | ||||||
|  |   void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } | ||||||
|  |   void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } | ||||||
|  |   void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } | ||||||
|  |   void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; } | ||||||
|  |   void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; } | ||||||
|  |   void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; } | ||||||
|  |  | ||||||
|  |   void loop() override; | ||||||
|  |  | ||||||
|  |   void update() override; | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   sensor::Sensor *voltage_sensor_; | ||||||
|  |   sensor::Sensor *current_sensor_1_; | ||||||
|  |   sensor::Sensor *current_sensor_2_; | ||||||
|  |   // NB This may be negative as the circuits is seemingly able to measure | ||||||
|  |   // power in both directions | ||||||
|  |   sensor::Sensor *power_sensor_1_; | ||||||
|  |   sensor::Sensor *power_sensor_2_; | ||||||
|  |   sensor::Sensor *energy_sensor_1_; | ||||||
|  |   sensor::Sensor *energy_sensor_2_; | ||||||
|  |   sensor::Sensor *energy_sensor_sum_; | ||||||
|  |  | ||||||
|  |   // Divide by this to turn into Watt | ||||||
|  |   float power_reference_ = BL0939_PREF; | ||||||
|  |   // Divide by this to turn into Volt | ||||||
|  |   float voltage_reference_ = BL0939_UREF; | ||||||
|  |   // Divide by this to turn into Ampere | ||||||
|  |   float current_reference_ = BL0939_IREF; | ||||||
|  |   // Divide by this to turn into kWh | ||||||
|  |   float energy_reference_ = BL0939_EREF; | ||||||
|  |  | ||||||
|  |   static uint32_t to_uint32_t(ube24_t input); | ||||||
|  |  | ||||||
|  |   static int32_t to_int32_t(sbe24_t input); | ||||||
|  |  | ||||||
|  |   static bool validate_checksum(const DataPacket *data); | ||||||
|  |  | ||||||
|  |   void received_package_(const DataPacket *data) const; | ||||||
|  | }; | ||||||
|  | }  // namespace bl0939 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										123
									
								
								esphome/components/bl0939/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bl0939/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import sensor, uart | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_VOLTAGE, | ||||||
|  |     DEVICE_CLASS_CURRENT, | ||||||
|  |     DEVICE_CLASS_ENERGY, | ||||||
|  |     DEVICE_CLASS_POWER, | ||||||
|  |     DEVICE_CLASS_VOLTAGE, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_AMPERE, | ||||||
|  |     UNIT_KILOWATT_HOURS, | ||||||
|  |     UNIT_VOLT, | ||||||
|  |     UNIT_WATT, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["uart"] | ||||||
|  |  | ||||||
|  | CONF_CURRENT_1 = "current_1" | ||||||
|  | CONF_CURRENT_2 = "current_2" | ||||||
|  | CONF_ACTIVE_POWER_1 = "active_power_1" | ||||||
|  | CONF_ACTIVE_POWER_2 = "active_power_2" | ||||||
|  | CONF_ENERGY_1 = "energy_1" | ||||||
|  | CONF_ENERGY_2 = "energy_2" | ||||||
|  | CONF_ENERGY_TOTAL = "energy_total" | ||||||
|  |  | ||||||
|  | bl0939_ns = cg.esphome_ns.namespace("bl0939") | ||||||
|  | BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(BL0939), | ||||||
|  |             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_VOLT, | ||||||
|  |                 accuracy_decimals=1, | ||||||
|  |                 device_class=DEVICE_CLASS_VOLTAGE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_CURRENT_1): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_AMPERE, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_CURRENT, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_CURRENT_2): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_AMPERE, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_CURRENT, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_WATT, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_POWER, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_WATT, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_POWER, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ENERGY_1): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_KILOWATT_HOURS, | ||||||
|  |                 accuracy_decimals=3, | ||||||
|  |                 device_class=DEVICE_CLASS_ENERGY, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ENERGY_2): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_KILOWATT_HOURS, | ||||||
|  |                 accuracy_decimals=3, | ||||||
|  |                 device_class=DEVICE_CLASS_ENERGY, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_KILOWATT_HOURS, | ||||||
|  |                 accuracy_decimals=3, | ||||||
|  |                 device_class=DEVICE_CLASS_ENERGY, | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("60s")) | ||||||
|  |     .extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await uart.register_uart_device(var, config) | ||||||
|  |  | ||||||
|  |     if CONF_VOLTAGE in config: | ||||||
|  |         conf = config[CONF_VOLTAGE] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_voltage_sensor(sens)) | ||||||
|  |     if CONF_CURRENT_1 in config: | ||||||
|  |         conf = config[CONF_CURRENT_1] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_current_sensor_1(sens)) | ||||||
|  |     if CONF_CURRENT_2 in config: | ||||||
|  |         conf = config[CONF_CURRENT_2] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_current_sensor_2(sens)) | ||||||
|  |     if CONF_ACTIVE_POWER_1 in config: | ||||||
|  |         conf = config[CONF_ACTIVE_POWER_1] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_power_sensor_1(sens)) | ||||||
|  |     if CONF_ACTIVE_POWER_2 in config: | ||||||
|  |         conf = config[CONF_ACTIVE_POWER_2] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_power_sensor_2(sens)) | ||||||
|  |     if CONF_ENERGY_1 in config: | ||||||
|  |         conf = config[CONF_ENERGY_1] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_energy_sensor_1(sens)) | ||||||
|  |     if CONF_ENERGY_2 in config: | ||||||
|  |         conf = config[CONF_ENERGY_2] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_energy_sensor_2(sens)) | ||||||
|  |     if CONF_ENERGY_TOTAL in config: | ||||||
|  |         conf = config[CONF_ENERGY_TOTAL] | ||||||
|  |         sens = await sensor.new_sensor(conf) | ||||||
|  |         cg.add(var.set_energy_sensor_sum(sens)) | ||||||
| @@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) { | |||||||
| void BME280Component::setup() { | void BME280Component::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up BME280..."); |   ESP_LOGCONFIG(TAG, "Setting up BME280..."); | ||||||
|   uint8_t chip_id = 0; |   uint8_t chip_id = 0; | ||||||
|  |  | ||||||
|  |   // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries | ||||||
|  |   // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. | ||||||
|  |   this->component_state_ &= ~COMPONENT_STATE_FAILED; | ||||||
|  |  | ||||||
|   if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { |   if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { | ||||||
|     this->error_code_ = COMMUNICATION_FAILED; |     this->error_code_ = COMMUNICATION_FAILED; | ||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|   | |||||||
| @@ -169,6 +169,14 @@ void BME680BSECComponent::loop() { | |||||||
|   } else { |   } else { | ||||||
|     this->status_clear_warning(); |     this->status_clear_warning(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Process a single action from the queue. These are primarily sensor state publishes | ||||||
|  |   // that in totality take too long to send in a single call. | ||||||
|  |   if (this->queue_.size()) { | ||||||
|  |     auto action = std::move(this->queue_.front()); | ||||||
|  |     this->queue_.pop(); | ||||||
|  |     action(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void BME680BSECComponent::run_() { | void BME680BSECComponent::run_() { | ||||||
| @@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme | |||||||
| } | } | ||||||
|  |  | ||||||
| void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { | void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { | ||||||
|   ESP_LOGV(TAG, "Publishing sensor states"); |   ESP_LOGV(TAG, "Queuing sensor state publish actions"); | ||||||
|   for (uint8_t i = 0; i < num_outputs; i++) { |   for (uint8_t i = 0; i < num_outputs; i++) { | ||||||
|  |     float signal = outputs[i].signal; | ||||||
|     switch (outputs[i].sensor_id) { |     switch (outputs[i].sensor_id) { | ||||||
|       case BSEC_OUTPUT_IAQ: |       case BSEC_OUTPUT_IAQ: | ||||||
|       case BSEC_OUTPUT_STATIC_IAQ: |       case BSEC_OUTPUT_STATIC_IAQ: { | ||||||
|         uint8_t accuracy; |         uint8_t accuracy = outputs[i].accuracy; | ||||||
|         accuracy = outputs[i].accuracy; |         this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); | ||||||
|         this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); |         this->queue_push_([this, accuracy]() { | ||||||
|         this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); |           this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); | ||||||
|         this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); |         }); | ||||||
|  |         this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); }); | ||||||
|  |  | ||||||
|         // Queue up an opportunity to save state |         // Queue up an opportunity to save state | ||||||
|         this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); |         this->queue_push_([this, accuracy]() { this->save_state_(accuracy); }); | ||||||
|         break; |       } break; | ||||||
|       case BSEC_OUTPUT_CO2_EQUIVALENT: |       case BSEC_OUTPUT_CO2_EQUIVALENT: | ||||||
|         this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); |         this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); | ||||||
|         break; |         break; | ||||||
|       case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: |       case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: | ||||||
|         this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); |         this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); | ||||||
|         break; |         break; | ||||||
|       case BSEC_OUTPUT_RAW_PRESSURE: |       case BSEC_OUTPUT_RAW_PRESSURE: | ||||||
|         this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); |         this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); | ||||||
|         break; |         break; | ||||||
|       case BSEC_OUTPUT_RAW_GAS: |       case BSEC_OUTPUT_RAW_GAS: | ||||||
|         this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); |         this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); | ||||||
|         break; |         break; | ||||||
|       case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: |       case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: | ||||||
|         this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); |         this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); | ||||||
|         break; |         break; | ||||||
|       case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: |       case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: | ||||||
|         this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); |         this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() { | |||||||
|   return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); |   return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { | void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { | ||||||
|   if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { |   if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   sensor->publish_state(value); |   sensor->publish_state(value); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { | void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { | ||||||
|   if (!sensor || (sensor->has_state() && sensor->state == value)) { |   if (!sensor || (sensor->has_state() && sensor->state == value)) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { | |||||||
|   void publish_(const bsec_output_t *outputs, uint8_t num_outputs); |   void publish_(const bsec_output_t *outputs, uint8_t num_outputs); | ||||||
|   int64_t get_time_ns_(); |   int64_t get_time_ns_(); | ||||||
|  |  | ||||||
|   void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); |   void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); | ||||||
|   void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); |   void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); | ||||||
|  |  | ||||||
|   void load_state_(); |   void load_state_(); | ||||||
|   void save_state_(uint8_t accuracy); |   void save_state_(uint8_t accuracy); | ||||||
|  |  | ||||||
|  |   void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); } | ||||||
|  |  | ||||||
|   struct bme680_dev bme680_; |   struct bme680_dev bme680_; | ||||||
|   bsec_library_return_t bsec_status_{BSEC_OK}; |   bsec_library_return_t bsec_status_{BSEC_OK}; | ||||||
|   int8_t bme680_status_{BME680_OK}; |   int8_t bme680_status_{BME680_OK}; | ||||||
| @@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { | |||||||
|   uint32_t millis_overflow_counter_{0}; |   uint32_t millis_overflow_counter_{0}; | ||||||
|   int64_t next_call_ns_{0}; |   int64_t next_call_ns_{0}; | ||||||
|  |  | ||||||
|  |   std::queue<std::function<void()>> queue_; | ||||||
|  |  | ||||||
|   ESPPreferenceObject bsec_state_; |   ESPPreferenceObject bsec_state_; | ||||||
|   uint32_t state_save_interval_ms_{21600000};  // 6 hours - 4 times a day |   uint32_t state_save_interval_ms_{21600000};  // 6 hours - 4 times a day | ||||||
|   uint32_t last_state_save_ms_ = 0; |   uint32_t last_state_save_ms_ = 0; | ||||||
|   | |||||||
| @@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema( | |||||||
|                     min=0, max=0x1FFFFFFF |                     min=0, max=0x1FFFFFFF | ||||||
|                 ), |                 ), | ||||||
|                 cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, |                 cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, | ||||||
|  |                 cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean, | ||||||
|             }, |             }, | ||||||
|             validate_id, |             validate_id, | ||||||
|         ), |         ), | ||||||
| @@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config): | |||||||
|         trigger = cg.new_Pvariable( |         trigger = cg.new_Pvariable( | ||||||
|             conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id |             conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id | ||||||
|         ) |         ) | ||||||
|  |         if CONF_REMOTE_TRANSMISSION_REQUEST in conf: | ||||||
|  |             cg.add( | ||||||
|  |                 trigger.set_remote_transmission_request( | ||||||
|  |                     conf[CONF_REMOTE_TRANSMISSION_REQUEST] | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|         await cg.register_component(trigger, conf) |         await cg.register_component(trigger, conf) | ||||||
|         await automation.build_automation( |         await automation.build_automation( | ||||||
|             trigger, |             trigger, | ||||||
|             [(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")], |             [ | ||||||
|  |                 (cg.std_vector.template(cg.uint8), "x"), | ||||||
|  |                 (cg.uint32, "can_id"), | ||||||
|  |                 (cg.bool_, "remote_transmission_request"), | ||||||
|  |             ], | ||||||
|             conf, |             conf, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -81,8 +81,10 @@ void Canbus::loop() { | |||||||
|     // fire all triggers |     // fire all triggers | ||||||
|     for (auto *trigger : this->triggers_) { |     for (auto *trigger : this->triggers_) { | ||||||
|       if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && |       if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && | ||||||
|           (trigger->use_extended_id_ == can_message.use_extended_id)) { |           (trigger->use_extended_id_ == can_message.use_extended_id) && | ||||||
|         trigger->trigger(data, can_message.can_id); |           (!trigger->remote_transmission_request_.has_value() || | ||||||
|  |            trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) { | ||||||
|  |         trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -126,13 +126,18 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P | |||||||
|   std::vector<uint8_t> data_static_{}; |   std::vector<uint8_t> data_static_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Component { | class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component { | ||||||
|   friend class Canbus; |   friend class Canbus; | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask, |   explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask, | ||||||
|                          const bool use_extended_id) |                          const bool use_extended_id) | ||||||
|       : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){}; |       : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){}; | ||||||
|  |  | ||||||
|  |   void set_remote_transmission_request(bool remote_transmission_request) { | ||||||
|  |     this->remote_transmission_request_ = remote_transmission_request; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void setup() override { this->parent_->add_trigger(this); } |   void setup() override { this->parent_->add_trigger(this); } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -140,6 +145,7 @@ class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Com | |||||||
|   uint32_t can_id_; |   uint32_t can_id_; | ||||||
|   uint32_t can_id_mask_; |   uint32_t can_id_mask_; | ||||||
|   bool use_extended_id_; |   bool use_extended_id_; | ||||||
|  |   optional<bool> remote_transmission_request_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace canbus | }  // namespace canbus | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace copy { | |||||||
| static const char *const TAG = "copy.select"; | static const char *const TAG = "copy.select"; | ||||||
|  |  | ||||||
| void CopySelect::setup() { | void CopySelect::setup() { | ||||||
|   source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); |   source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); | ||||||
|  |  | ||||||
|   traits.set_options(source_->traits.get_options()); |   traits.set_options(source_->traits.get_options()); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N | |||||||
|         config = { |         config = { | ||||||
|             "substitutions": {"name": name}, |             "substitutions": {"name": name}, | ||||||
|             "packages": {project_name: import_url}, |             "packages": {project_name: import_url}, | ||||||
|             "esphome": {"name_add_mac_suffix": False}, |             "esphome": { | ||||||
|  |                 "name": "${name}", | ||||||
|  |                 "name_add_mac_suffix": False, | ||||||
|  |             }, | ||||||
|         } |         } | ||||||
|         p.write_text( |         p.write_text( | ||||||
|             dump(config) + WIFI_CONFIG, |             dump(config) + WIFI_CONFIG, | ||||||
|   | |||||||
| @@ -93,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") | |||||||
| DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) | DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) | ||||||
| EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) | EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) | ||||||
| PreventDeepSleepAction = deep_sleep_ns.class_( | PreventDeepSleepAction = deep_sleep_ns.class_( | ||||||
|     "PreventDeepSleepAction", automation.Action |     "PreventDeepSleepAction", | ||||||
|  |     automation.Action, | ||||||
|  |     cg.Parented.template(DeepSleepComponent), | ||||||
|  | ) | ||||||
|  | AllowDeepSleepAction = deep_sleep_ns.class_( | ||||||
|  |     "AllowDeepSleepAction", | ||||||
|  |     automation.Action, | ||||||
|  |     cg.Parented.template(DeepSleepComponent), | ||||||
| ) | ) | ||||||
|  |  | ||||||
| WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") | WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") | ||||||
| @@ -208,29 +215,33 @@ async def to_code(config): | |||||||
|     cg.add_define("USE_DEEP_SLEEP") |     cg.add_define("USE_DEEP_SLEEP") | ||||||
|  |  | ||||||
|  |  | ||||||
| DEEP_SLEEP_ENTER_SCHEMA = cv.All( | DEEP_SLEEP_ACTION_SCHEMA = cv.Schema( | ||||||
|     automation.maybe_simple_id( |  | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.use_id(DeepSleepComponent), |         cv.GenerateID(): cv.use_id(DeepSleepComponent), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | DEEP_SLEEP_ENTER_SCHEMA = cv.All( | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         DEEP_SLEEP_ACTION_SCHEMA.extend( | ||||||
|  |             cv.Schema( | ||||||
|  |                 { | ||||||
|                     cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( |                     cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( | ||||||
|                         cv.positive_time_period_milliseconds |                         cv.positive_time_period_milliseconds | ||||||
|                     ), |                     ), | ||||||
|                     # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep |                     # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep | ||||||
|             cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day), |                     cv.Exclusive(CONF_UNTIL, "time"): cv.All( | ||||||
|  |                         cv.only_on_esp32, cv.time_of_day | ||||||
|  |                     ), | ||||||
|                     cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), |                     cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||||
|                 } |                 } | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|     ), |     ), | ||||||
|     cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), |     cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id( |  | ||||||
|     { |  | ||||||
|         cv.GenerateID(): cv.use_id(DeepSleepComponent), |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @automation.register_action( | @automation.register_action( | ||||||
|     "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA |     "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA | ||||||
| ) | ) | ||||||
| @@ -252,8 +263,16 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): | |||||||
|  |  | ||||||
|  |  | ||||||
| @automation.register_action( | @automation.register_action( | ||||||
|     "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA |     "deep_sleep.prevent", | ||||||
|  |     PreventDeepSleepAction, | ||||||
|  |     automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), | ||||||
| ) | ) | ||||||
| async def deep_sleep_prevent_to_code(config, action_id, template_arg, args): | @automation.register_action( | ||||||
|     paren = await cg.get_variable(config[CONF_ID]) |     "deep_sleep.allow", | ||||||
|     return cg.new_Pvariable(action_id, template_arg, paren) |     AllowDeepSleepAction, | ||||||
|  |     automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), | ||||||
|  | ) | ||||||
|  | async def deep_sleep_action_to_code(config, action_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     return var | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ optional<uint32_t> DeepSleepComponent::get_run_duration_() const { | |||||||
|     switch (wakeup_cause) { |     switch (wakeup_cause) { | ||||||
|       case ESP_SLEEP_WAKEUP_EXT0: |       case ESP_SLEEP_WAKEUP_EXT0: | ||||||
|       case ESP_SLEEP_WAKEUP_EXT1: |       case ESP_SLEEP_WAKEUP_EXT1: | ||||||
|  |       case ESP_SLEEP_WAKEUP_GPIO: | ||||||
|         return this->wakeup_cause_to_run_duration_->gpio_cause; |         return this->wakeup_cause_to_run_duration_->gpio_cause; | ||||||
|       case ESP_SLEEP_WAKEUP_TOUCHPAD: |       case ESP_SLEEP_WAKEUP_TOUCHPAD: | ||||||
|         return this->wakeup_cause_to_run_duration_->touch_cause; |         return this->wakeup_cause_to_run_duration_->touch_cause; | ||||||
| @@ -72,16 +73,27 @@ float DeepSleepComponent::get_loop_priority() const { | |||||||
|   return -100.0f;  // run after everything else is ready |   return -100.0f;  // run after everything else is ready | ||||||
| } | } | ||||||
| void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } | void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } | ||||||
| #ifdef USE_ESP32 | #if defined(USE_ESP32) | ||||||
| void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { | void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { | ||||||
|   this->wakeup_pin_mode_ = wakeup_pin_mode; |   this->wakeup_pin_mode_ = wakeup_pin_mode; | ||||||
| } | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #if defined(USE_ESP32) | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32C3) | ||||||
|  |  | ||||||
| void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } | void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } | ||||||
|  |  | ||||||
| void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } | void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } | ||||||
|  |  | ||||||
|  | #endif | ||||||
|  |  | ||||||
| void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { | void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { | ||||||
|   wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; |   wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; | ||||||
| } | } | ||||||
|  |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } | void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } | ||||||
| void DeepSleepComponent::begin_sleep(bool manual) { | void DeepSleepComponent::begin_sleep(bool manual) { | ||||||
|   if (this->prevent_ && !manual) { |   if (this->prevent_ && !manual) { | ||||||
| @@ -107,7 +119,8 @@ void DeepSleepComponent::begin_sleep(bool manual) { | |||||||
|  |  | ||||||
|   App.run_safe_shutdown_hooks(); |   App.run_safe_shutdown_hooks(); | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) | #if defined(USE_ESP32) | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32C3) | ||||||
|   if (this->sleep_duration_.has_value()) |   if (this->sleep_duration_.has_value()) | ||||||
|     esp_sleep_enable_timer_wakeup(*this->sleep_duration_); |     esp_sleep_enable_timer_wakeup(*this->sleep_duration_); | ||||||
|   if (this->wakeup_pin_ != nullptr) { |   if (this->wakeup_pin_ != nullptr) { | ||||||
| @@ -125,10 +138,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { | |||||||
|     esp_sleep_enable_touchpad_wakeup(); |     esp_sleep_enable_touchpad_wakeup(); | ||||||
|     esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); |     esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   esp_deep_sleep_start(); |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_VARIANT_ESP32C3 | #ifdef USE_ESP32_VARIANT_ESP32C3 | ||||||
|   if (this->sleep_duration_.has_value()) |   if (this->sleep_duration_.has_value()) | ||||||
|     esp_sleep_enable_timer_wakeup(*this->sleep_duration_); |     esp_sleep_enable_timer_wakeup(*this->sleep_duration_); | ||||||
| @@ -137,9 +147,12 @@ void DeepSleepComponent::begin_sleep(bool manual) { | |||||||
|     if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { |     if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { | ||||||
|       level = !level; |       level = !level; | ||||||
|     } |     } | ||||||
|     esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); |     esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), | ||||||
|  |                                       static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level)); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |   esp_deep_sleep_start(); | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
|   ESP.deepSleep(*this->sleep_duration_);  // NOLINT(readability-static-accessed-through-instance) |   ESP.deepSleep(*this->sleep_duration_);  // NOLINT(readability-static-accessed-through-instance) | ||||||
| @@ -147,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { | |||||||
| } | } | ||||||
| float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } | float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } | ||||||
| void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } | void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } | ||||||
|  | void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } | ||||||
|  |  | ||||||
| }  // namespace deep_sleep | }  // namespace deep_sleep | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -70,17 +70,19 @@ class DeepSleepComponent : public Component { | |||||||
|   void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); |   void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) | #if defined(USE_ESP32) | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32C3) | ||||||
|  |  | ||||||
|   void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); |   void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); | ||||||
|  |  | ||||||
|   void set_touch_wakeup(bool touch_wakeup); |   void set_touch_wakeup(bool touch_wakeup); | ||||||
|  |  | ||||||
|  | #endif | ||||||
|   // Set the duration in ms for how long the code should run before entering |   // Set the duration in ms for how long the code should run before entering | ||||||
|   // deep sleep mode, according to the cause the ESP32 has woken. |   // deep sleep mode, according to the cause the ESP32 has woken. | ||||||
|   void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); |   void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); | ||||||
|  |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   /// Set a duration in ms for how long the code should run before entering deep sleep mode. |   /// Set a duration in ms for how long the code should run before entering deep sleep mode. | ||||||
|   void set_run_duration(uint32_t time_ms); |   void set_run_duration(uint32_t time_ms); | ||||||
|  |  | ||||||
| @@ -94,6 +96,7 @@ class DeepSleepComponent : public Component { | |||||||
|   void begin_sleep(bool manual = false); |   void begin_sleep(bool manual = false); | ||||||
|  |  | ||||||
|   void prevent_deep_sleep(); |   void prevent_deep_sleep(); | ||||||
|  |   void allow_deep_sleep(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   // Returns nullopt if no run duration is set. Otherwise, returns the run |   // Returns nullopt if no run duration is set. Otherwise, returns the run | ||||||
| @@ -187,14 +190,14 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> { | |||||||
| #endif | #endif | ||||||
| }; | }; | ||||||
|  |  | ||||||
| template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> { | template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { | ||||||
|  public: |  public: | ||||||
|   PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} |   void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|   void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } | template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { | ||||||
|  |  public: | ||||||
|  protected: |   void play(Ts... x) override { this->parent_->allow_deep_sleep(); } | ||||||
|   DeepSleepComponent *deep_sleep_; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace deep_sleep | }  // namespace deep_sleep | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/delonghi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/delonghi/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@grob6000"] | ||||||
							
								
								
									
										20
									
								
								esphome/components/delonghi/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/delonghi/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import climate_ir | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["climate_ir"] | ||||||
|  |  | ||||||
|  | delonghi_ns = cg.esphome_ns.namespace("delonghi") | ||||||
|  | DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(DelonghiClimate), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await climate_ir.register_climate_ir(var, config) | ||||||
							
								
								
									
										186
									
								
								esphome/components/delonghi/delonghi.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								esphome/components/delonghi/delonghi.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | |||||||
|  | #include "delonghi.h" | ||||||
|  | #include "esphome/components/remote_base/remote_base.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace delonghi { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "delonghi.climate"; | ||||||
|  |  | ||||||
|  | void DelonghiClimate::transmit_state() { | ||||||
|  |   uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0}; | ||||||
|  |   remote_state[0] = DELONGHI_ADDRESS; | ||||||
|  |   remote_state[1] = this->temperature_(); | ||||||
|  |   remote_state[1] |= (this->fan_speed_()) << 5; | ||||||
|  |   remote_state[2] = this->operation_mode_(); | ||||||
|  |   // Calculate checksum | ||||||
|  |   for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) { | ||||||
|  |     remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto transmit = this->transmitter_->transmit(); | ||||||
|  |   auto *data = transmit.get_data(); | ||||||
|  |   data->set_carrier_frequency(DELONGHI_IR_FREQUENCY); | ||||||
|  |  | ||||||
|  |   data->mark(DELONGHI_HEADER_MARK); | ||||||
|  |   data->space(DELONGHI_HEADER_SPACE); | ||||||
|  |   for (unsigned char b : remote_state) { | ||||||
|  |     for (uint8_t mask = 1; mask > 0; mask <<= 1) {  // iterate through bit mask | ||||||
|  |       data->mark(DELONGHI_BIT_MARK); | ||||||
|  |       bool bit = b & mask; | ||||||
|  |       data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   data->mark(DELONGHI_BIT_MARK); | ||||||
|  |   data->space(0); | ||||||
|  |  | ||||||
|  |   transmit.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t DelonghiClimate::operation_mode_() { | ||||||
|  |   uint8_t operating_mode = DELONGHI_MODE_ON; | ||||||
|  |   switch (this->mode) { | ||||||
|  |     case climate::CLIMATE_MODE_COOL: | ||||||
|  |       operating_mode |= DELONGHI_MODE_COOL; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_MODE_DRY: | ||||||
|  |       operating_mode |= DELONGHI_MODE_DRY; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_MODE_HEAT: | ||||||
|  |       operating_mode |= DELONGHI_MODE_HEAT; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_MODE_HEAT_COOL: | ||||||
|  |       operating_mode |= DELONGHI_MODE_AUTO; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_MODE_FAN_ONLY: | ||||||
|  |       operating_mode |= DELONGHI_MODE_FAN; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_MODE_OFF: | ||||||
|  |     default: | ||||||
|  |       operating_mode = DELONGHI_MODE_OFF; | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |   return operating_mode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint16_t DelonghiClimate::fan_speed_() { | ||||||
|  |   uint16_t fan_speed; | ||||||
|  |   switch (this->fan_mode.value()) { | ||||||
|  |     case climate::CLIMATE_FAN_LOW: | ||||||
|  |       fan_speed = DELONGHI_FAN_LOW; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_FAN_MEDIUM: | ||||||
|  |       fan_speed = DELONGHI_FAN_MEDIUM; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_FAN_HIGH: | ||||||
|  |       fan_speed = DELONGHI_FAN_HIGH; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_FAN_AUTO: | ||||||
|  |     default: | ||||||
|  |       fan_speed = DELONGHI_FAN_AUTO; | ||||||
|  |   } | ||||||
|  |   return fan_speed; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t DelonghiClimate::temperature_() { | ||||||
|  |   // Force special temperatures depending on the mode | ||||||
|  |   uint8_t temperature = 0b0001; | ||||||
|  |   switch (this->mode) { | ||||||
|  |     case climate::CLIMATE_MODE_HEAT: | ||||||
|  |       temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT; | ||||||
|  |       break; | ||||||
|  |     case climate::CLIMATE_MODE_COOL: | ||||||
|  |     case climate::CLIMATE_MODE_DRY: | ||||||
|  |     case climate::CLIMATE_MODE_HEAT_COOL: | ||||||
|  |     case climate::CLIMATE_MODE_FAN_ONLY: | ||||||
|  |     case climate::CLIMATE_MODE_OFF: | ||||||
|  |     default: | ||||||
|  |       temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL; | ||||||
|  |   } | ||||||
|  |   if (temperature > 0x0F) { | ||||||
|  |     temperature = 0x0F;  // clamp maximum | ||||||
|  |   } | ||||||
|  |   return temperature; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) { | ||||||
|  |   uint8_t checksum = 0; | ||||||
|  |   for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) { | ||||||
|  |     checksum += frame[i]; | ||||||
|  |   } | ||||||
|  |   if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   uint8_t mode = frame[2] & 0x0F; | ||||||
|  |   if (mode & DELONGHI_MODE_ON) { | ||||||
|  |     switch (mode & 0x0E) { | ||||||
|  |       case DELONGHI_MODE_COOL: | ||||||
|  |         this->mode = climate::CLIMATE_MODE_COOL; | ||||||
|  |         break; | ||||||
|  |       case DELONGHI_MODE_DRY: | ||||||
|  |         this->mode = climate::CLIMATE_MODE_DRY; | ||||||
|  |         break; | ||||||
|  |       case DELONGHI_MODE_HEAT: | ||||||
|  |         this->mode = climate::CLIMATE_MODE_HEAT; | ||||||
|  |         break; | ||||||
|  |       case DELONGHI_MODE_AUTO: | ||||||
|  |         this->mode = climate::CLIMATE_MODE_HEAT_COOL; | ||||||
|  |         break; | ||||||
|  |       case DELONGHI_MODE_FAN: | ||||||
|  |         this->mode = climate::CLIMATE_MODE_FAN_ONLY; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     this->mode = climate::CLIMATE_MODE_OFF; | ||||||
|  |   } | ||||||
|  |   uint8_t temperature = frame[1] & 0x0F; | ||||||
|  |   if (this->mode == climate::CLIMATE_MODE_HEAT) { | ||||||
|  |     this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT; | ||||||
|  |   } else { | ||||||
|  |     this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL; | ||||||
|  |   } | ||||||
|  |   uint8_t fan_mode = frame[1] >> 5; | ||||||
|  |   switch (fan_mode) { | ||||||
|  |     case DELONGHI_FAN_LOW: | ||||||
|  |       this->fan_mode = climate::CLIMATE_FAN_LOW; | ||||||
|  |       break; | ||||||
|  |     case DELONGHI_FAN_MEDIUM: | ||||||
|  |       this->fan_mode = climate::CLIMATE_FAN_MEDIUM; | ||||||
|  |       break; | ||||||
|  |     case DELONGHI_FAN_HIGH: | ||||||
|  |       this->fan_mode = climate::CLIMATE_FAN_HIGH; | ||||||
|  |       break; | ||||||
|  |     case DELONGHI_FAN_AUTO: | ||||||
|  |       this->fan_mode = climate::CLIMATE_FAN_AUTO; | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |   this->publish_state(); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) { | ||||||
|  |   uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {}; | ||||||
|  |   if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) { | ||||||
|  |     uint8_t byte = 0; | ||||||
|  |     for (int8_t bit = 0; bit < 8; bit++) { | ||||||
|  |       if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) { | ||||||
|  |         byte |= 1 << bit; | ||||||
|  |       } else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     state_frame[pos] = byte; | ||||||
|  |     if (pos == 0) { | ||||||
|  |       // frame header | ||||||
|  |       if (byte != DELONGHI_ADDRESS) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return this->parse_state_frame_(state_frame); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace delonghi | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										64
									
								
								esphome/components/delonghi/delonghi.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/delonghi/delonghi.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/climate_ir/climate_ir.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace delonghi { | ||||||
|  |  | ||||||
|  | // Values for DELONGHI ARC43XXX IR Controllers | ||||||
|  | const uint8_t DELONGHI_ADDRESS = 83; | ||||||
|  |  | ||||||
|  | // Temperature | ||||||
|  | const uint8_t DELONGHI_TEMP_MIN = 13;          // Celsius | ||||||
|  | const uint8_t DELONGHI_TEMP_MAX = 32;          // Celsius | ||||||
|  | const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17;  // Celsius | ||||||
|  | const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12;  // Celsius | ||||||
|  |  | ||||||
|  | // Modes | ||||||
|  | const uint8_t DELONGHI_MODE_AUTO = 0b1000; | ||||||
|  | const uint8_t DELONGHI_MODE_COOL = 0b0000; | ||||||
|  | const uint8_t DELONGHI_MODE_HEAT = 0b0110; | ||||||
|  | const uint8_t DELONGHI_MODE_DRY = 0b0010; | ||||||
|  | const uint8_t DELONGHI_MODE_FAN = 0b0100; | ||||||
|  | const uint8_t DELONGHI_MODE_OFF = 0b0000; | ||||||
|  | const uint8_t DELONGHI_MODE_ON = 0b0001; | ||||||
|  |  | ||||||
|  | // Fan Speed | ||||||
|  | const uint8_t DELONGHI_FAN_AUTO = 0b00; | ||||||
|  | const uint8_t DELONGHI_FAN_HIGH = 0b01; | ||||||
|  | const uint8_t DELONGHI_FAN_MEDIUM = 0b10; | ||||||
|  | const uint8_t DELONGHI_FAN_LOW = 0b11; | ||||||
|  |  | ||||||
|  | // IR Transmission - similar to NEC1 | ||||||
|  | const uint32_t DELONGHI_IR_FREQUENCY = 38000; | ||||||
|  | const uint32_t DELONGHI_HEADER_MARK = 9000; | ||||||
|  | const uint32_t DELONGHI_HEADER_SPACE = 4500; | ||||||
|  | const uint32_t DELONGHI_BIT_MARK = 465; | ||||||
|  | const uint32_t DELONGHI_ONE_SPACE = 1750; | ||||||
|  | const uint32_t DELONGHI_ZERO_SPACE = 670; | ||||||
|  |  | ||||||
|  | // State Frame size | ||||||
|  | const uint8_t DELONGHI_STATE_FRAME_SIZE = 8; | ||||||
|  |  | ||||||
|  | class DelonghiClimate : public climate_ir::ClimateIR { | ||||||
|  |  public: | ||||||
|  |   DelonghiClimate() | ||||||
|  |       : climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true, | ||||||
|  |                               {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, | ||||||
|  |                                climate::CLIMATE_FAN_HIGH}, | ||||||
|  |                               {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, | ||||||
|  |                                climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   // Transmit via IR the state of this climate controller. | ||||||
|  |   void transmit_state() override; | ||||||
|  |   uint8_t operation_mode_(); | ||||||
|  |   uint16_t fan_speed_(); | ||||||
|  |   uint8_t temperature_(); | ||||||
|  |   // Handle received IR Buffer | ||||||
|  |   bool on_receive(remote_base::RemoteReceiveData data) override; | ||||||
|  |   bool parse_state_frame_(const uint8_t frame[]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace delonghi | ||||||
|  | }  // namespace esphome | ||||||
| @@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|  |     case IMAGE_TYPE_RGB565: | ||||||
|  |       for (int img_x = 0; img_x < image->get_width(); img_x++) { | ||||||
|  |         for (int img_y = 0; img_y < image->get_height(); img_y++) { | ||||||
|  |           this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const { | |||||||
|                            (progmem_read_byte(this->data_start_ + pos + 0) << 16); |                            (progmem_read_byte(this->data_start_ + pos + 0) << 16); | ||||||
|   return Color(color32); |   return Color(color32); | ||||||
| } | } | ||||||
|  | Color Image::get_rgb565_pixel(int x, int y) const { | ||||||
|  |   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) | ||||||
|  |     return Color::BLACK; | ||||||
|  |   const uint32_t pos = (x + y * this->width_) * 2; | ||||||
|  |   uint16_t rgb565 = | ||||||
|  |       progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); | ||||||
|  |   auto r = (rgb565 & 0xF800) >> 11; | ||||||
|  |   auto g = (rgb565 & 0x07E0) >> 5; | ||||||
|  |   auto b = rgb565 & 0x001F; | ||||||
|  |   return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); | ||||||
|  | } | ||||||
| Color Image::get_grayscale_pixel(int x, int y) const { | Color Image::get_grayscale_pixel(int x, int y) const { | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) | ||||||
|     return Color::BLACK; |     return Color::BLACK; | ||||||
| @@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const { | |||||||
|                            (progmem_read_byte(this->data_start_ + pos + 0) << 16); |                            (progmem_read_byte(this->data_start_ + pos + 0) << 16); | ||||||
|   return Color(color32); |   return Color(color32); | ||||||
| } | } | ||||||
|  | Color Animation::get_rgb565_pixel(int x, int y) const { | ||||||
|  |   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) | ||||||
|  |     return Color::BLACK; | ||||||
|  |   const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; | ||||||
|  |   if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) | ||||||
|  |     return Color::BLACK; | ||||||
|  |   const uint32_t pos = (x + y * this->width_ + frame_index) * 2; | ||||||
|  |   uint16_t rgb565 = | ||||||
|  |       progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); | ||||||
|  |   auto r = (rgb565 & 0xF800) >> 11; | ||||||
|  |   auto g = (rgb565 & 0x07E0) >> 5; | ||||||
|  |   auto b = rgb565 & 0x001F; | ||||||
|  |   return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); | ||||||
|  | } | ||||||
| Color Animation::get_grayscale_pixel(int x, int y) const { | Color Animation::get_grayscale_pixel(int x, int y) const { | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) | ||||||
|     return Color::BLACK; |     return Color::BLACK; | ||||||
| @@ -552,6 +584,12 @@ void Animation::next_frame() { | |||||||
|     this->current_frame_ = 0; |     this->current_frame_ = 0; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | void Animation::prev_frame() { | ||||||
|  |   this->current_frame_--; | ||||||
|  |   if (this->current_frame_ < 0) { | ||||||
|  |     this->current_frame_ = this->animation_frame_count_ - 1; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} | DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} | ||||||
| void DisplayPage::show() { this->parent_->show_page(this); } | void DisplayPage::show() { this->parent_->show_page(this); } | ||||||
|   | |||||||
| @@ -82,6 +82,7 @@ enum ImageType { | |||||||
|   IMAGE_TYPE_GRAYSCALE = 1, |   IMAGE_TYPE_GRAYSCALE = 1, | ||||||
|   IMAGE_TYPE_RGB24 = 2, |   IMAGE_TYPE_RGB24 = 2, | ||||||
|   IMAGE_TYPE_TRANSPARENT_BINARY = 3, |   IMAGE_TYPE_TRANSPARENT_BINARY = 3, | ||||||
|  |   IMAGE_TYPE_RGB565 = 4, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum DisplayRotation { | enum DisplayRotation { | ||||||
| @@ -453,6 +454,7 @@ class Image { | |||||||
|   Image(const uint8_t *data_start, int width, int height, ImageType type); |   Image(const uint8_t *data_start, int width, int height, ImageType type); | ||||||
|   virtual bool get_pixel(int x, int y) const; |   virtual bool get_pixel(int x, int y) const; | ||||||
|   virtual Color get_color_pixel(int x, int y) const; |   virtual Color get_color_pixel(int x, int y) const; | ||||||
|  |   virtual Color get_rgb565_pixel(int x, int y) const; | ||||||
|   virtual Color get_grayscale_pixel(int x, int y) const; |   virtual Color get_grayscale_pixel(int x, int y) const; | ||||||
|   int get_width() const; |   int get_width() const; | ||||||
|   int get_height() const; |   int get_height() const; | ||||||
| @@ -470,11 +472,13 @@ class Animation : public Image { | |||||||
|   Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); |   Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); | ||||||
|   bool get_pixel(int x, int y) const override; |   bool get_pixel(int x, int y) const override; | ||||||
|   Color get_color_pixel(int x, int y) const override; |   Color get_color_pixel(int x, int y) const override; | ||||||
|  |   Color get_rgb565_pixel(int x, int y) const override; | ||||||
|   Color get_grayscale_pixel(int x, int y) const override; |   Color get_grayscale_pixel(int x, int y) const override; | ||||||
|  |  | ||||||
|   int get_animation_frame_count() const; |   int get_animation_frame_count() const; | ||||||
|   int get_current_frame() const; |   int get_current_frame() const; | ||||||
|   void next_frame(); |   void next_frame(); | ||||||
|  |   void prev_frame(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   int current_frame_; |   int current_frame_; | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								esphome/components/ens210/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/ens210/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										230
									
								
								esphome/components/ens210/ens210.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								esphome/components/ens210/ens210.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,230 @@ | |||||||
|  | // ENS210 relative humidity and temperature sensor with I2C interface from ScioSense | ||||||
|  | // | ||||||
|  | // Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf | ||||||
|  | // | ||||||
|  | // Implementation based on: | ||||||
|  | //   https://github.com/maarten-pennings/ENS210 | ||||||
|  | //   https://github.com/sciosense/ENS210_driver | ||||||
|  |  | ||||||
|  | #include "ens210.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ens210 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "ens210"; | ||||||
|  |  | ||||||
|  | // ENS210 chip constants | ||||||
|  | static const uint8_t ENS210_BOOTING_MS = 2;  // Booting time in ms (also after reset, or going to high power) | ||||||
|  | static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS = | ||||||
|  |     130;                                        // Conversion time in ms for single shot T/H measurement | ||||||
|  | static const uint16_t ENS210_PART_ID = 0x0210;  // The expected part id of the ENS210 | ||||||
|  |  | ||||||
|  | // Addresses of the ENS210 registers | ||||||
|  | static const uint8_t ENS210_REGISTER_PART_ID = 0x00; | ||||||
|  | static const uint8_t ENS210_REGISTER_UID = 0x04; | ||||||
|  | static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10; | ||||||
|  | static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11; | ||||||
|  | static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21; | ||||||
|  | static const uint8_t ENS210_REGISTER_SENS_START = 0x22; | ||||||
|  | static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23; | ||||||
|  | static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24; | ||||||
|  | static const uint8_t ENS210_REGISTER_T_VAL = 0x30; | ||||||
|  | static const uint8_t ENS210_REGISTER_H_VAL = 0x33; | ||||||
|  |  | ||||||
|  | // CRC-7 constants | ||||||
|  | static const uint8_t CRC7_WIDTH = 7;    // A 7 bits CRC has polynomial of 7th order, which has 8 terms | ||||||
|  | static const uint8_t CRC7_POLY = 0x89;  // The 8 coefficients of the polynomial | ||||||
|  | static const uint8_t CRC7_IVEC = 0x7F;  // Initial vector has all 7 bits high | ||||||
|  |  | ||||||
|  | // Payload data constants | ||||||
|  | static const uint8_t DATA7_WIDTH = 17; | ||||||
|  | static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1);  // 0b 0 1111 1111 1111 1111 | ||||||
|  | static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1));   // 0b 1 0000 0000 0000 0000 | ||||||
|  |  | ||||||
|  | // Converts a status to a human readable string | ||||||
|  | static const LogString *ens210_status_to_human(int status) { | ||||||
|  |   switch (status) { | ||||||
|  |     case ENS210Component::ENS210_STATUS_I2C_ERROR: | ||||||
|  |       return LOG_STR("I2C error - communication with ENS210 failed!"); | ||||||
|  |     case ENS210Component::ENS210_STATUS_CRC_ERROR: | ||||||
|  |       return LOG_STR("CRC error"); | ||||||
|  |     case ENS210Component::ENS210_STATUS_INVALID: | ||||||
|  |       return LOG_STR("Invalid data"); | ||||||
|  |     case ENS210Component::ENS210_STATUS_OK: | ||||||
|  |       return LOG_STR("Status OK"); | ||||||
|  |     case ENS210Component::ENS210_WRONG_CHIP_ID: | ||||||
|  |       return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?"); | ||||||
|  |     default: | ||||||
|  |       return LOG_STR("Unknown"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Compute the CRC-7 of 'value' (should only have 17 bits) | ||||||
|  | // https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation | ||||||
|  | static uint32_t crc7(uint32_t value) { | ||||||
|  |   // Setup polynomial | ||||||
|  |   uint32_t polynomial = CRC7_POLY; | ||||||
|  |   // Align polynomial with data | ||||||
|  |   polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1); | ||||||
|  |   // Loop variable (indicates which bit to test, start with highest) | ||||||
|  |   uint32_t bit = DATA7_MSB; | ||||||
|  |   // Make room for CRC value | ||||||
|  |   value = value << CRC7_WIDTH; | ||||||
|  |   bit = bit << CRC7_WIDTH; | ||||||
|  |   polynomial = polynomial << CRC7_WIDTH; | ||||||
|  |   // Insert initial vector | ||||||
|  |   value |= CRC7_IVEC; | ||||||
|  |   // Apply division until all bits done | ||||||
|  |   while (bit & (DATA7_MASK << CRC7_WIDTH)) { | ||||||
|  |     if (bit & value) | ||||||
|  |       value ^= polynomial; | ||||||
|  |     bit >>= 1; | ||||||
|  |     polynomial >>= 1; | ||||||
|  |   } | ||||||
|  |   return value; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ENS210Component::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Setting up ENS210..."); | ||||||
|  |   uint8_t data[2]; | ||||||
|  |   uint16_t part_id = 0; | ||||||
|  |   // Reset | ||||||
|  |   if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) { | ||||||
|  |     this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80); | ||||||
|  |     this->error_code_ = ENS210_STATUS_I2C_ERROR; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // Wait to boot after reset | ||||||
|  |   delay(ENS210_BOOTING_MS); | ||||||
|  |   // Must disable low power to read PART_ID | ||||||
|  |   if (!set_low_power_(false)) { | ||||||
|  |     // Try to go back to default mode (low power enabled) | ||||||
|  |     set_low_power_(true); | ||||||
|  |     this->error_code_ = ENS210_STATUS_I2C_ERROR; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // Read the PART_ID | ||||||
|  |   if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) { | ||||||
|  |     // Try to go back to default mode (low power enabled) | ||||||
|  |     set_low_power_(true); | ||||||
|  |     this->error_code_ = ENS210_STATUS_I2C_ERROR; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // Pack bytes into partid | ||||||
|  |   part_id = data[1] * 256U + data[0] * 1U; | ||||||
|  |   // Check expected part id of the ENS210 | ||||||
|  |   if (part_id != ENS210_PART_ID) { | ||||||
|  |     this->error_code_ = ENS210_WRONG_CHIP_ID; | ||||||
|  |     this->mark_failed(); | ||||||
|  |   } | ||||||
|  |   // Set default power mode (low power enabled) | ||||||
|  |   set_low_power_(true); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ENS210Component::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "ENS210:"); | ||||||
|  |   LOG_I2C_DEVICE(this); | ||||||
|  |   if (this->is_failed()) { | ||||||
|  |     ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_))); | ||||||
|  |   } | ||||||
|  |   LOG_UPDATE_INTERVAL(this); | ||||||
|  |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } | ||||||
|  |  | ||||||
|  | void ENS210Component::update() { | ||||||
|  |   // Execute a single measurement | ||||||
|  |   if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { | ||||||
|  |     ESP_LOGE(TAG, "Starting single measurement failed!"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // Trigger measurement | ||||||
|  |   if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) { | ||||||
|  |     ESP_LOGE(TAG, "Trigger of measurement failed!"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // Wait for measurement to complete | ||||||
|  |   this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() { | ||||||
|  |     int temperature_data, temperature_status, humidity_data, humidity_status; | ||||||
|  |     uint8_t data[6]; | ||||||
|  |     uint32_t h_val_data, t_val_data; | ||||||
|  |     // Set default status for early bail out | ||||||
|  |     temperature_status = ENS210_STATUS_I2C_ERROR; | ||||||
|  |     humidity_status = ENS210_STATUS_I2C_ERROR; | ||||||
|  |  | ||||||
|  |     // Read T_VAL and H_VAL | ||||||
|  |     if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) { | ||||||
|  |       ESP_LOGE(TAG, "Communication with ENS210 failed!"); | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     // Pack bytes for humidity | ||||||
|  |     h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]); | ||||||
|  |     // Extract humidity data and update the status | ||||||
|  |     extract_measurement_(h_val_data, &humidity_data, &humidity_status); | ||||||
|  |  | ||||||
|  |     if (humidity_status == ENS210_STATUS_OK) { | ||||||
|  |       if (this->humidity_sensor_ != nullptr) { | ||||||
|  |         float humidity = (humidity_data & 0xFFFF) / 512.0; | ||||||
|  |         this->humidity_sensor_->publish_state(humidity); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status))); | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     // Pack bytes for temperature | ||||||
|  |     t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]); | ||||||
|  |     // Extract temperature data and update the status | ||||||
|  |     extract_measurement_(t_val_data, &temperature_data, &temperature_status); | ||||||
|  |  | ||||||
|  |     if (temperature_status == ENS210_STATUS_OK) { | ||||||
|  |       if (this->temperature_sensor_ != nullptr) { | ||||||
|  |         // Temperature in Celsius | ||||||
|  |         float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0; | ||||||
|  |         this->temperature_sensor_->publish_state(temperature); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status))); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Extracts measurement 'data' and 'status' from a 'val' obtained from measurment. | ||||||
|  | void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) { | ||||||
|  |   *data = (val >> 0) & 0xffff; | ||||||
|  |   int valid = (val >> 16) & 0x1; | ||||||
|  |   uint32_t crc = (val >> 17) & 0x7f; | ||||||
|  |   uint32_t payload = (val >> 0) & 0x1ffff; | ||||||
|  |   // Check CRC | ||||||
|  |   uint8_t crc_ok = crc7(payload) == crc; | ||||||
|  |  | ||||||
|  |   if (!crc_ok) { | ||||||
|  |     *status = ENS210_STATUS_CRC_ERROR; | ||||||
|  |   } else if (!valid) { | ||||||
|  |     *status = ENS210_STATUS_INVALID; | ||||||
|  |   } else { | ||||||
|  |     *status = ENS210_STATUS_OK; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems. | ||||||
|  | bool ENS210Component::set_low_power_(bool enable) { | ||||||
|  |   uint8_t low_power_cmd = enable ? 0x01 : 0x00; | ||||||
|  |   ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false"); | ||||||
|  |   bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd); | ||||||
|  |   delay(ENS210_BOOTING_MS); | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace ens210 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										39
									
								
								esphome/components/ens210/ens210.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/ens210/ens210.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/i2c/i2c.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ens210 { | ||||||
|  |  | ||||||
|  | /// This class implements support for the ENS210 relative humidity and temperature i2c sensor. | ||||||
|  | class ENS210Component : public PollingComponent, public i2c::I2CDevice { | ||||||
|  |  public: | ||||||
|  |   float get_setup_priority() const override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void setup() override; | ||||||
|  |   void update() override; | ||||||
|  |  | ||||||
|  |   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } | ||||||
|  |   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } | ||||||
|  |  | ||||||
|  |   enum ErrorCode { | ||||||
|  |     ENS210_STATUS_OK = 0,     // The value was read, the CRC matches, and data is valid | ||||||
|  |     ENS210_STATUS_INVALID,    // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was | ||||||
|  |                               // not yet finished) | ||||||
|  |     ENS210_STATUS_CRC_ERROR,  // The value was read, but the CRC over the payload (valid and data) does not match | ||||||
|  |     ENS210_STATUS_I2C_ERROR,  // There was an I2C communication error | ||||||
|  |     ENS210_WRONG_CHIP_ID      // The read PART_ID is not the expected part id of the ENS210 | ||||||
|  |   } error_code_{ENS210_STATUS_OK}; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool set_low_power_(bool enable); | ||||||
|  |   void extract_measurement_(uint32_t val, int *data, int *status); | ||||||
|  |  | ||||||
|  |   sensor::Sensor *temperature_sensor_; | ||||||
|  |   sensor::Sensor *humidity_sensor_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace ens210 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										58
									
								
								esphome/components/ens210/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/ens210/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import i2c, sensor | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_HUMIDITY, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_HUMIDITY, | ||||||
|  |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_CELSIUS, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@itn3rd77"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  |  | ||||||
|  | ens210_ns = cg.esphome_ns.namespace("ens210") | ||||||
|  |  | ||||||
|  | ENS210Component = ens210_ns.class_( | ||||||
|  |     "ENS210Component", cg.PollingComponent, i2c.I2CDevice | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(ENS210Component), | ||||||
|  |             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |                 accuracy_decimals=1, | ||||||
|  |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PERCENT, | ||||||
|  |                 accuracy_decimals=1, | ||||||
|  |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("60s")) | ||||||
|  |     .extend(i2c.i2c_device_schema(0x43)) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
|  |  | ||||||
|  |     if CONF_TEMPERATURE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||||
|  |         cg.add(var.set_temperature_sensor(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_HUMIDITY in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||||
|  |         cg.add(var.set_humidity_sensor(sens)) | ||||||
| @@ -107,7 +107,7 @@ def validate_gpio_pin(value): | |||||||
|     value = _translate_pin(value) |     value = _translate_pin(value) | ||||||
|     variant = CORE.data[KEY_ESP32][KEY_VARIANT] |     variant = CORE.data[KEY_ESP32][KEY_VARIANT] | ||||||
|     if variant not in _esp32_validations: |     if variant not in _esp32_validations: | ||||||
|         raise cv.Invalid("Unsupported ESP32 variant {variant}") |         raise cv.Invalid(f"Unsupported ESP32 variant {variant}") | ||||||
|  |  | ||||||
|     return _esp32_validations[variant].pin_validation(value) |     return _esp32_validations[variant].pin_validation(value) | ||||||
|  |  | ||||||
| @@ -121,7 +121,7 @@ def validate_supports(value): | |||||||
|     is_pulldown = mode[CONF_PULLDOWN] |     is_pulldown = mode[CONF_PULLDOWN] | ||||||
|     variant = CORE.data[KEY_ESP32][KEY_VARIANT] |     variant = CORE.data[KEY_ESP32][KEY_VARIANT] | ||||||
|     if variant not in _esp32_validations: |     if variant not in _esp32_validations: | ||||||
|         raise cv.Invalid("Unsupported ESP32 variant {variant}") |         raise cv.Invalid(f"Unsupported ESP32 variant {variant}") | ||||||
|  |  | ||||||
|     if is_open_drain and not is_output: |     if is_open_drain and not is_output: | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed | |||||||
|  |  | ||||||
| from .const import ( | from .const import ( | ||||||
|     CONF_RESTORE_FROM_FLASH, |     CONF_RESTORE_FROM_FLASH, | ||||||
|  |     CONF_EARLY_PIN_INIT, | ||||||
|     KEY_BOARD, |     KEY_BOARD, | ||||||
|     KEY_ESP8266, |     KEY_ESP8266, | ||||||
|     KEY_PIN_INITIAL_STATES, |     KEY_PIN_INITIAL_STATES, | ||||||
| @@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Required(CONF_BOARD): cv.string_strict, |             cv.Required(CONF_BOARD): cv.string_strict, | ||||||
|             cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, |             cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, | ||||||
|             cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, |             cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, | ||||||
|  |             cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean, | ||||||
|             cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( |             cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( | ||||||
|                 *BUILD_FLASH_MODES, lower=True |                 *BUILD_FLASH_MODES, lower=True | ||||||
|             ), |             ), | ||||||
| @@ -197,6 +199,9 @@ async def to_code(config): | |||||||
|     if config[CONF_RESTORE_FROM_FLASH]: |     if config[CONF_RESTORE_FROM_FLASH]: | ||||||
|         cg.add_define("USE_ESP8266_PREFERENCES_FLASH") |         cg.add_define("USE_ESP8266_PREFERENCES_FLASH") | ||||||
|  |  | ||||||
|  |     if config[CONF_EARLY_PIN_INIT]: | ||||||
|  |         cg.add_define("USE_ESP8266_EARLY_PIN_INIT") | ||||||
|  |  | ||||||
|     # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when |     # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when | ||||||
|     # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make |     # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make | ||||||
|     # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of |     # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266" | |||||||
| KEY_BOARD = "board" | KEY_BOARD = "board" | ||||||
| KEY_PIN_INITIAL_STATES = "pin_initial_states" | KEY_PIN_INITIAL_STATES = "pin_initial_states" | ||||||
| CONF_RESTORE_FROM_FLASH = "restore_from_flash" | CONF_RESTORE_FROM_FLASH = "restore_from_flash" | ||||||
|  | CONF_EARLY_PIN_INIT = "early_pin_init" | ||||||
|  |  | ||||||
| # esp8266 namespace is already defined by arduino, manually prefix esphome | # esp8266 namespace is already defined by arduino, manually prefix esphome | ||||||
| esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") | esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
|  |  | ||||||
| #include "core.h" | #include "core.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "preferences.h" | #include "preferences.h" | ||||||
| @@ -55,6 +56,7 @@ extern "C" void resetPins() {  // NOLINT | |||||||
|   // ourselves and this causes pins to toggle during reboot. |   // ourselves and this causes pins to toggle during reboot. | ||||||
|   force_link_symbols(); |   force_link_symbols(); | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266_EARLY_PIN_INIT | ||||||
|   for (int i = 0; i < 16; i++) { |   for (int i = 0; i < 16; i++) { | ||||||
|     uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; |     uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; | ||||||
|     uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; |     uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; | ||||||
| @@ -63,6 +65,7 @@ extern "C" void resetPins() {  // NOLINT | |||||||
|     if (level != 255) |     if (level != 255) | ||||||
|       digitalWrite(i, level);  // NOLINT |       digitalWrite(i, level);  // NOLINT | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ IMAGE_TYPE = { | |||||||
|     "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, |     "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, | ||||||
|     "RGB24": ImageType.IMAGE_TYPE_RGB24, |     "RGB24": ImageType.IMAGE_TYPE_RGB24, | ||||||
|     "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, |     "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, | ||||||
|  |     "RGB565": ImageType.IMAGE_TYPE_RGB565, | ||||||
| } | } | ||||||
|  |  | ||||||
| Image_ = display.display_ns.class_("Image") | Image_ = display.display_ns.class_("Image") | ||||||
| @@ -89,6 +90,21 @@ async def to_code(config): | |||||||
|             data[pos] = pix[2] |             data[pos] = pix[2] | ||||||
|             pos += 1 |             pos += 1 | ||||||
|  |  | ||||||
|  |     elif config[CONF_TYPE] == "RGB565": | ||||||
|  |         image = image.convert("RGB") | ||||||
|  |         pixels = list(image.getdata()) | ||||||
|  |         data = [0 for _ in range(height * width * 3)] | ||||||
|  |         pos = 0 | ||||||
|  |         for pix in pixels: | ||||||
|  |             R = pix[0] >> 3 | ||||||
|  |             G = pix[1] >> 2 | ||||||
|  |             B = pix[2] >> 3 | ||||||
|  |             rgb = (R << 11) | (G << 5) | B | ||||||
|  |             data[pos] = rgb >> 8 | ||||||
|  |             pos += 1 | ||||||
|  |             data[pos] = rgb & 255 | ||||||
|  |             pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] == "BINARY": |     elif config[CONF_TYPE] == "BINARY": | ||||||
|         image = image.convert("1", dither=dither) |         image = image.convert("1", dither=dither) | ||||||
|         width8 = ((width + 7) // 8) * 8 |         width8 = ((width + 7) // 8) * 8 | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component { | |||||||
|   void write_data_(std::vector<uint8_t> &data); |   void write_data_(std::vector<uint8_t> &data); | ||||||
|  |  | ||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
|   HardwareSerial *hw_serial_{nullptr}; |   Stream *hw_serial_{nullptr}; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP_IDF | #ifdef USE_ESP_IDF | ||||||
|   uart_port_t uart_num_; |   uart_port_t uart_num_; | ||||||
|   | |||||||
| @@ -19,8 +19,13 @@ from esphome.const import ( | |||||||
|     CONF_TX_BUFFER_SIZE, |     CONF_TX_BUFFER_SIZE, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority | from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority | ||||||
| from esphome.components.esp32 import get_esp32_variant | from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||||
| from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3 | from esphome.components.esp32.const import ( | ||||||
|  |     VARIANT_ESP32, | ||||||
|  |     VARIANT_ESP32S2, | ||||||
|  |     VARIANT_ESP32C3, | ||||||
|  |     VARIANT_ESP32S3, | ||||||
|  | ) | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| logger_ns = cg.esphome_ns.namespace("logger") | logger_ns = cg.esphome_ns.namespace("logger") | ||||||
| @@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [ | |||||||
|     "VERY_VERBOSE", |     "VERY_VERBOSE", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2] | UART0 = "UART0" | ||||||
|  | UART1 = "UART1" | ||||||
|  | UART2 = "UART2" | ||||||
|  | UART0_SWAP = "UART0_SWAP" | ||||||
|  | USB_SERIAL_JTAG = "USB_SERIAL_JTAG" | ||||||
|  | USB_CDC = "USB_CDC" | ||||||
|  |  | ||||||
| UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"] | UART_SELECTION_ESP32 = { | ||||||
|  |     VARIANT_ESP32: [UART0, UART1, UART2], | ||||||
|  |     VARIANT_ESP32S2: [UART0, UART1, USB_CDC], | ||||||
|  |     VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], | ||||||
|  |     VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG], | ||||||
|  | } | ||||||
|  |  | ||||||
| UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"] | UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] | ||||||
|  |  | ||||||
| UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"] | ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] | ||||||
|  |  | ||||||
| HARDWARE_UART_TO_UART_SELECTION = { | HARDWARE_UART_TO_UART_SELECTION = { | ||||||
|     "UART0": logger_ns.UART_SELECTION_UART0, |     UART0: logger_ns.UART_SELECTION_UART0, | ||||||
|     "UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP, |     UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, | ||||||
|     "UART1": logger_ns.UART_SELECTION_UART1, |     UART1: logger_ns.UART_SELECTION_UART1, | ||||||
|     "UART2": logger_ns.UART_SELECTION_UART2, |     UART2: logger_ns.UART_SELECTION_UART2, | ||||||
|  |     USB_CDC: logger_ns.UART_SELECTION_USB_CDC, | ||||||
|  |     USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG, | ||||||
| } | } | ||||||
|  |  | ||||||
| HARDWARE_UART_TO_SERIAL = { | HARDWARE_UART_TO_SERIAL = { | ||||||
|     "UART0": cg.global_ns.Serial, |     UART0: cg.global_ns.Serial, | ||||||
|     "UART0_SWAP": cg.global_ns.Serial, |     UART0_SWAP: cg.global_ns.Serial, | ||||||
|     "UART1": cg.global_ns.Serial1, |     UART1: cg.global_ns.Serial1, | ||||||
|     "UART2": cg.global_ns.Serial2, |     UART2: cg.global_ns.Serial2, | ||||||
| } | } | ||||||
|  |  | ||||||
| is_log_level = cv.one_of(*LOG_LEVELS, upper=True) | is_log_level = cv.one_of(*LOG_LEVELS, upper=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| def uart_selection(value): | def uart_selection(value): | ||||||
|  |     if value.upper() in ESP_IDF_UARTS: | ||||||
|  |         if not CORE.using_esp_idf: | ||||||
|  |             raise cv.Invalid(f"Only esp-idf framework supports {value}.") | ||||||
|     if CORE.is_esp32: |     if CORE.is_esp32: | ||||||
|         if get_esp32_variant() in ESP32_REDUCED_VARIANTS: |         variant = get_esp32_variant() | ||||||
|             return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value) |         if variant in UART_SELECTION_ESP32: | ||||||
|         return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value) |             return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) | ||||||
|     if CORE.is_esp8266: |     if CORE.is_esp8266: | ||||||
|         return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) |         return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) | ||||||
|     raise NotImplementedError |     raise NotImplementedError | ||||||
| @@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, |             cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, | ||||||
|             cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, |             cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, | ||||||
|             cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, |             cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, | ||||||
|             cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection, |             cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection, | ||||||
|             cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, |             cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, | ||||||
|             cv.Optional(CONF_LOGS, default={}): cv.Schema( |             cv.Optional(CONF_LOGS, default={}): cv.Schema( | ||||||
|                 { |                 { | ||||||
| @@ -185,6 +205,12 @@ async def to_code(config): | |||||||
|     if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): |     if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): | ||||||
|         cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") |         cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") | ||||||
|  |  | ||||||
|  |     if CORE.using_esp_idf: | ||||||
|  |         if config[CONF_HARDWARE_UART] == USB_CDC: | ||||||
|  |             add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) | ||||||
|  |         elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: | ||||||
|  |             add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) | ||||||
|  |  | ||||||
|     # Register at end for safe mode |     # Register at end for safe mode | ||||||
|     await cg.register_component(log, config) |     await cg.register_component(log, config) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -116,8 +116,22 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { | |||||||
|     this->hw_serial_->println(msg); |     this->hw_serial_->println(msg); | ||||||
| #endif  // USE_ARDUINO | #endif  // USE_ARDUINO | ||||||
| #ifdef USE_ESP_IDF | #ifdef USE_ESP_IDF | ||||||
|  |     if ( | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32S2) | ||||||
|  |         uart_ == UART_SELECTION_USB_CDC | ||||||
|  | #elif defined(USE_ESP32_VARIANT_ESP32C3) | ||||||
|  |         uart_ == UART_SELECTION_USB_SERIAL_JTAG | ||||||
|  | #elif defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |         uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG | ||||||
|  | #else | ||||||
|  |         /* DISABLES CODE */ (false) | ||||||
|  | #endif | ||||||
|  |     ) { | ||||||
|  |       puts(msg); | ||||||
|  |     } else { | ||||||
|       uart_write_bytes(uart_num_, msg, strlen(msg)); |       uart_write_bytes(uart_num_, msg, strlen(msg)); | ||||||
|       uart_write_bytes(uart_num_, "\n", 1); |       uart_write_bytes(uart_num_, "\n", 1); | ||||||
|  |     } | ||||||
| #endif | #endif | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -149,13 +163,25 @@ void Logger::pre_setup() { | |||||||
|       case UART_SELECTION_UART0_SWAP: |       case UART_SELECTION_UART0_SWAP: | ||||||
| #endif | #endif | ||||||
|         this->hw_serial_ = &Serial; |         this->hw_serial_ = &Serial; | ||||||
|  |         Serial.begin(this->baud_rate_); | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |         if (this->uart_ == UART_SELECTION_UART0_SWAP) { | ||||||
|  |           Serial.swap(); | ||||||
|  |         } | ||||||
|  |         Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); | ||||||
|  | #endif | ||||||
|         break; |         break; | ||||||
|       case UART_SELECTION_UART1: |       case UART_SELECTION_UART1: | ||||||
|         this->hw_serial_ = &Serial1; |         this->hw_serial_ = &Serial1; | ||||||
|  |         Serial1.begin(this->baud_rate_); | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |         Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); | ||||||
|  | #endif | ||||||
|         break; |         break; | ||||||
| #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) | #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) | ||||||
|       case UART_SELECTION_UART2: |       case UART_SELECTION_UART2: | ||||||
|         this->hw_serial_ = &Serial2; |         this->hw_serial_ = &Serial2; | ||||||
|  |         Serial2.begin(this->baud_rate_); | ||||||
|         break; |         break; | ||||||
| #endif | #endif | ||||||
|     } |     } | ||||||
| @@ -169,12 +195,23 @@ void Logger::pre_setup() { | |||||||
|       case UART_SELECTION_UART1: |       case UART_SELECTION_UART1: | ||||||
|         uart_num_ = UART_NUM_1; |         uart_num_ = UART_NUM_1; | ||||||
|         break; |         break; | ||||||
| #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) | #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|       case UART_SELECTION_UART2: |       case UART_SELECTION_UART2: | ||||||
|         uart_num_ = UART_NUM_2; |         uart_num_ = UART_NUM_2; | ||||||
|         break; |         break; | ||||||
| #endif | #endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |       case UART_SELECTION_USB_CDC: | ||||||
|  |         uart_num_ = -1; | ||||||
|  |         break; | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |       case UART_SELECTION_USB_SERIAL_JTAG: | ||||||
|  |         uart_num_ = -1; | ||||||
|  |         break; | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 | ||||||
|     } |     } | ||||||
|  |     if (uart_num_ >= 0) { | ||||||
|       uart_config_t uart_config{}; |       uart_config_t uart_config{}; | ||||||
|       uart_config.baud_rate = (int) baud_rate_; |       uart_config.baud_rate = (int) baud_rate_; | ||||||
|       uart_config.data_bits = UART_DATA_8_BITS; |       uart_config.data_bits = UART_DATA_8_BITS; | ||||||
| @@ -185,23 +222,14 @@ void Logger::pre_setup() { | |||||||
|       const int uart_buffer_size = tx_buffer_size_; |       const int uart_buffer_size = tx_buffer_size_; | ||||||
|       // Install UART driver using an event queue here |       // Install UART driver using an event queue here | ||||||
|       uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); |       uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_ARDUINO |  | ||||||
|     this->hw_serial_->begin(this->baud_rate_); |  | ||||||
| #ifdef USE_ESP8266 |  | ||||||
|     if (this->uart_ == UART_SELECTION_UART0_SWAP) { |  | ||||||
|       this->hw_serial_->swap(); |  | ||||||
|     } |     } | ||||||
|     this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); | #endif  // USE_ESP_IDF | ||||||
| #endif |  | ||||||
| #endif  // USE_ARDUINO |  | ||||||
|   } |   } | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
|   else { |   else { | ||||||
|     uart_set_debug(UART_NO); |     uart_set_debug(UART_NO); | ||||||
|   } |   } | ||||||
| #endif | #endif  // USE_ESP8266 | ||||||
|  |  | ||||||
|   global_logger = this; |   global_logger = this; | ||||||
| #if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) | #if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) | ||||||
| @@ -209,7 +237,7 @@ void Logger::pre_setup() { | |||||||
|   if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { |   if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { | ||||||
|     esp_log_level_set("*", ESP_LOG_VERBOSE); |     esp_log_level_set("*", ESP_LOG_VERBOSE); | ||||||
|   } |   } | ||||||
| #endif | #endif  // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO | ||||||
|  |  | ||||||
|   ESP_LOGI(TAG, "Log initialized"); |   ESP_LOGI(TAG, "Log initialized"); | ||||||
| } | } | ||||||
| @@ -224,11 +252,24 @@ void Logger::add_on_log_callback(std::function<void(int, const char *, const cha | |||||||
| float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } | float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } | ||||||
| const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; | const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; | const char *const UART_SELECTIONS[] = { | ||||||
| #endif |     "UART0",           "UART1", | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |     "UART2", | ||||||
|  | #endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 | ||||||
|  | #if defined(USE_ESP_IDF) | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |     "USB_CDC", | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |     "USB_SERIAL_JTAG", | ||||||
|  | #endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 | ||||||
|  | #endif  // USE_ESP_IDF | ||||||
|  | }; | ||||||
|  | #endif  // USE_ESP32 | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
| const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; | const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; | ||||||
| #endif | #endif  // USE_ESP8266 | ||||||
| void Logger::dump_config() { | void Logger::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "Logger:"); |   ESP_LOGCONFIG(TAG, "Logger:"); | ||||||
|   ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); |   ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); | ||||||
|   | |||||||
| @@ -24,9 +24,19 @@ namespace logger { | |||||||
| enum UARTSelection { | enum UARTSelection { | ||||||
|   UART_SELECTION_UART0 = 0, |   UART_SELECTION_UART0 = 0, | ||||||
|   UART_SELECTION_UART1, |   UART_SELECTION_UART1, | ||||||
| #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) | #if defined(USE_ESP32) | ||||||
|  | #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|   UART_SELECTION_UART2, |   UART_SELECTION_UART2, | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ESP_IDF | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |   UART_SELECTION_USB_CDC, | ||||||
|  | #endif | ||||||
|  | #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) | ||||||
|  |   UART_SELECTION_USB_SERIAL_JTAG, | ||||||
|  | #endif | ||||||
|  | #endif | ||||||
|  | #endif | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
|   UART_SELECTION_UART0_SWAP, |   UART_SELECTION_UART0_SWAP, | ||||||
| #endif | #endif | ||||||
| @@ -40,7 +50,7 @@ class Logger : public Component { | |||||||
|   void set_baud_rate(uint32_t baud_rate); |   void set_baud_rate(uint32_t baud_rate); | ||||||
|   uint32_t get_baud_rate() const { return baud_rate_; } |   uint32_t get_baud_rate() const { return baud_rate_; } | ||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
|   HardwareSerial *get_hw_serial() const { return hw_serial_; } |   Stream *get_hw_serial() const { return hw_serial_; } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP_IDF | #ifdef USE_ESP_IDF | ||||||
|   uart_port_t get_uart_num() const { return uart_num_; } |   uart_port_t get_uart_num() const { return uart_num_; } | ||||||
| @@ -119,7 +129,7 @@ class Logger : public Component { | |||||||
|   int tx_buffer_size_{0}; |   int tx_buffer_size_{0}; | ||||||
|   UARTSelection uart_{UART_SELECTION_UART0}; |   UARTSelection uart_{UART_SELECTION_UART0}; | ||||||
| #ifdef USE_ARDUINO | #ifdef USE_ARDUINO | ||||||
|   HardwareSerial *hw_serial_{nullptr}; |   Stream *hw_serial_{nullptr}; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_ESP_IDF | #ifdef USE_ESP_IDF | ||||||
|   uart_port_t uart_num_; |   uart_port_t uart_num_; | ||||||
|   | |||||||
| @@ -52,7 +52,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) | |||||||
|  |  | ||||||
|   // Now parse the data - See Datasheet for definition |   // Now parse the data - See Datasheet for definition | ||||||
|  |  | ||||||
|   if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP) { |   if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP && | ||||||
|  |       static_cast<SensorType>(manu_data.data[0]) != PLUS_BOTTOM_UP) { | ||||||
|     ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); |     ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -12,7 +12,8 @@ namespace mopeka_pro_check { | |||||||
| enum SensorType { | enum SensorType { | ||||||
|   STANDARD_BOTTOM_UP = 0x03, |   STANDARD_BOTTOM_UP = 0x03, | ||||||
|   TOP_DOWN_AIR_ABOVE = 0x04, |   TOP_DOWN_AIR_ABOVE = 0x04, | ||||||
|   BOTTOM_UP_WATER = 0x05 |   BOTTOM_UP_WATER = 0x05, | ||||||
|  |   PLUS_BOTTOM_UP = 0x08 | ||||||
|   // all other values are reserved |   // all other values are reserved | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -556,7 +556,12 @@ void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; } | |||||||
|  |  | ||||||
| void MQTTClientComponent::disable_discovery() { | void MQTTClientComponent::disable_discovery() { | ||||||
|   this->discovery_info_ = MQTTDiscoveryInfo{ |   this->discovery_info_ = MQTTDiscoveryInfo{ | ||||||
|       .prefix = "", .retain = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR}; |       .prefix = "", | ||||||
|  |       .retain = false, | ||||||
|  |       .clean = false, | ||||||
|  |       .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, | ||||||
|  |       .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, | ||||||
|  |   }; | ||||||
| } | } | ||||||
| void MQTTClientComponent::on_shutdown() { | void MQTTClientComponent::on_shutdown() { | ||||||
|   if (!this->shutdown_message_.topic.empty()) { |   if (!this->shutdown_message_.topic.empty()) { | ||||||
|   | |||||||
| @@ -277,6 +277,7 @@ class MQTTClientComponent : public Component { | |||||||
|       .retain = true, |       .retain = true, | ||||||
|       .clean = false, |       .clean = false, | ||||||
|       .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, |       .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, | ||||||
|  |       .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, | ||||||
|   }; |   }; | ||||||
|   std::string topic_prefix_{}; |   std::string topic_prefix_{}; | ||||||
|   MQTTMessage log_message_; |   MQTTMessage log_message_; | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() { | |||||||
|     call.set_option(state); |     call.set_option(state); | ||||||
|     call.perform(); |     call.perform(); | ||||||
|   }); |   }); | ||||||
|   this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); |   this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTSelectComponent::dump_config() { | void MQTTSelectComponent::dump_config() { | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ from esphome.const import ( | |||||||
|     CONF_UNIT_OF_MEASUREMENT, |     CONF_UNIT_OF_MEASUREMENT, | ||||||
|     CONF_MQTT_ID, |     CONF_MQTT_ID, | ||||||
|     CONF_VALUE, |     CONF_VALUE, | ||||||
|  |     CONF_OPERATION, | ||||||
|  |     CONF_CYCLE, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
| from esphome.cpp_helpers import setup_entity | from esphome.cpp_helpers import setup_entity | ||||||
| @@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_( | |||||||
|  |  | ||||||
| # Actions | # Actions | ||||||
| NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) | NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) | ||||||
|  | NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action) | ||||||
|  |  | ||||||
| # Conditions | # Conditions | ||||||
| NumberInRangeCondition = number_ns.class_( | NumberInRangeCondition = number_ns.class_( | ||||||
| @@ -49,6 +52,15 @@ NUMBER_MODES = { | |||||||
|     "SLIDER": NumberMode.NUMBER_MODE_SLIDER, |     "SLIDER": NumberMode.NUMBER_MODE_SLIDER, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | NumberOperation = number_ns.enum("NumberOperation") | ||||||
|  |  | ||||||
|  | NUMBER_OPERATION_OPTIONS = { | ||||||
|  |     "INCREMENT": NumberOperation.NUMBER_OP_INCREMENT, | ||||||
|  |     "DECREMENT": NumberOperation.NUMBER_OP_DECREMENT, | ||||||
|  |     "TO_MIN": NumberOperation.NUMBER_OP_TO_MIN, | ||||||
|  |     "TO_MAX": NumberOperation.NUMBER_OP_TO_MAX, | ||||||
|  | } | ||||||
|  |  | ||||||
| icon = cv.icon | icon = cv.icon | ||||||
|  |  | ||||||
| NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | ||||||
| @@ -159,12 +171,18 @@ async def to_code(config): | |||||||
|     cg.add_global(number_ns.using) |     cg.add_global(number_ns.using) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | OPERATION_BASE_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(Number), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @automation.register_action( | @automation.register_action( | ||||||
|     "number.set", |     "number.set", | ||||||
|     NumberSetAction, |     NumberSetAction, | ||||||
|     cv.Schema( |     OPERATION_BASE_SCHEMA.extend( | ||||||
|         { |         { | ||||||
|             cv.Required(CONF_ID): cv.use_id(Number), |  | ||||||
|             cv.Required(CONF_VALUE): cv.templatable(cv.float_), |             cv.Required(CONF_VALUE): cv.templatable(cv.float_), | ||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
| @@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args): | |||||||
|     template_ = await cg.templatable(config[CONF_VALUE], args, float) |     template_ = await cg.templatable(config[CONF_VALUE], args, float) | ||||||
|     cg.add(var.set_value(template_)) |     cg.add(var.set_value(template_)) | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "number.increment", | ||||||
|  |     NumberOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of( | ||||||
|  |                     "INCREMENT", upper=True | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional(CONF_CYCLE, default=True): cv.boolean, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "number.decrement", | ||||||
|  |     NumberOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of( | ||||||
|  |                     "DECREMENT", upper=True | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional(CONF_CYCLE, default=True): cv.boolean, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "number.to_min", | ||||||
|  |     NumberOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of( | ||||||
|  |                     "TO_MIN", upper=True | ||||||
|  |                 ), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "number.to_max", | ||||||
|  |     NumberOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of( | ||||||
|  |                     "TO_MAX", upper=True | ||||||
|  |                 ), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "number.operation", | ||||||
|  |     NumberOperationAction, | ||||||
|  |     OPERATION_BASE_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_OPERATION): cv.templatable( | ||||||
|  |                 cv.enum(NUMBER_OPERATION_OPTIONS, upper=True) | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | async def number_to_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) | ||||||
|  |     if CONF_OPERATION in config: | ||||||
|  |         to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation) | ||||||
|  |         cg.add(var.set_operation(to_)) | ||||||
|  |         if CONF_CYCLE in config: | ||||||
|  |             cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) | ||||||
|  |             cg.add(var.set_cycle(cycle_)) | ||||||
|  |     if CONF_MODE in config: | ||||||
|  |         cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]])) | ||||||
|  |         if CONF_CYCLE in config: | ||||||
|  |             cg.add(var.set_cycle(config[CONF_CYCLE])) | ||||||
|  |     return var | ||||||
|   | |||||||
| @@ -29,6 +29,25 @@ template<typename... Ts> class NumberSetAction : public Action<Ts...> { | |||||||
|   Number *number_; |   Number *number_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class NumberOperationAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit NumberOperationAction(Number *number) : number_(number) {} | ||||||
|  |   TEMPLATABLE_VALUE(NumberOperation, operation) | ||||||
|  |   TEMPLATABLE_VALUE(bool, cycle) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     auto call = this->number_->make_call(); | ||||||
|  |     call.with_operation(this->operation_.value(x...)); | ||||||
|  |     if (this->cycle_.has_value()) { | ||||||
|  |       call.with_cycle(this->cycle_.value(x...)); | ||||||
|  |     } | ||||||
|  |     call.perform(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   Number *number_; | ||||||
|  | }; | ||||||
|  |  | ||||||
| class ValueRangeTrigger : public Trigger<float>, public Component { | class ValueRangeTrigger : public Trigger<float>, public Component { | ||||||
|  public: |  public: | ||||||
|   explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} |   explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} | ||||||
|   | |||||||
| @@ -6,30 +6,6 @@ namespace number { | |||||||
|  |  | ||||||
| static const char *const TAG = "number"; | static const char *const TAG = "number"; | ||||||
|  |  | ||||||
| void NumberCall::perform() { |  | ||||||
|   ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); |  | ||||||
|   if (!this->value_.has_value() || std::isnan(*this->value_)) { |  | ||||||
|     ESP_LOGW(TAG, "No value set for NumberCall"); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const auto &traits = this->parent_->traits; |  | ||||||
|   auto value = *this->value_; |  | ||||||
|  |  | ||||||
|   float min_value = traits.get_min_value(); |  | ||||||
|   if (value < min_value) { |  | ||||||
|     ESP_LOGW(TAG, "  Value %f must not be less than minimum %f", value, min_value); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   float max_value = traits.get_max_value(); |  | ||||||
|   if (value > max_value) { |  | ||||||
|     ESP_LOGW(TAG, "  Value %f must not be greater than maximum %f", value, max_value); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   ESP_LOGD(TAG, "  Value: %f", *this->value_); |  | ||||||
|   this->parent_->control(*this->value_); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void Number::publish_state(float state) { | void Number::publish_state(float state) { | ||||||
|   this->has_state_ = true; |   this->has_state_ = true; | ||||||
|   this->state = state; |   this->state = state; | ||||||
| @@ -41,15 +17,6 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) { | |||||||
|   this->state_callback_.add(std::move(callback)); |   this->state_callback_.add(std::move(callback)); | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string NumberTraits::get_unit_of_measurement() { |  | ||||||
|   if (this->unit_of_measurement_.has_value()) |  | ||||||
|     return *this->unit_of_measurement_; |  | ||||||
|   return ""; |  | ||||||
| } |  | ||||||
| void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { |  | ||||||
|   this->unit_of_measurement_ = unit_of_measurement; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| uint32_t Number::hash_base() { return 2282307003UL; } | uint32_t Number::hash_base() { return 2282307003UL; } | ||||||
|  |  | ||||||
| }  // namespace number | }  // namespace number | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/entity_base.h" | #include "esphome/core/entity_base.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  | #include "number_call.h" | ||||||
|  | #include "number_traits.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace number { | namespace number { | ||||||
| @@ -20,54 +22,6 @@ namespace number { | |||||||
|  |  | ||||||
| class Number; | class Number; | ||||||
|  |  | ||||||
| class NumberCall { |  | ||||||
|  public: |  | ||||||
|   explicit NumberCall(Number *parent) : parent_(parent) {} |  | ||||||
|   void perform(); |  | ||||||
|  |  | ||||||
|   NumberCall &set_value(float value) { |  | ||||||
|     value_ = value; |  | ||||||
|     return *this; |  | ||||||
|   } |  | ||||||
|   const optional<float> &get_value() const { return value_; } |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   Number *const parent_; |  | ||||||
|   optional<float> value_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| enum NumberMode : uint8_t { |  | ||||||
|   NUMBER_MODE_AUTO = 0, |  | ||||||
|   NUMBER_MODE_BOX = 1, |  | ||||||
|   NUMBER_MODE_SLIDER = 2, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class NumberTraits { |  | ||||||
|  public: |  | ||||||
|   void set_min_value(float min_value) { min_value_ = min_value; } |  | ||||||
|   float get_min_value() const { return min_value_; } |  | ||||||
|   void set_max_value(float max_value) { max_value_ = max_value; } |  | ||||||
|   float get_max_value() const { return max_value_; } |  | ||||||
|   void set_step(float step) { step_ = step; } |  | ||||||
|   float get_step() const { return step_; } |  | ||||||
|  |  | ||||||
|   /// Get the unit of measurement, using the manual override if set. |  | ||||||
|   std::string get_unit_of_measurement(); |  | ||||||
|   /// Manually set the unit of measurement. |  | ||||||
|   void set_unit_of_measurement(const std::string &unit_of_measurement); |  | ||||||
|  |  | ||||||
|   // Get/set the frontend mode. |  | ||||||
|   NumberMode get_mode() const { return this->mode_; } |  | ||||||
|   void set_mode(NumberMode mode) { this->mode_ = mode; } |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   float min_value_ = NAN; |  | ||||||
|   float max_value_ = NAN; |  | ||||||
|   float step_ = NAN; |  | ||||||
|   optional<std::string> unit_of_measurement_;  ///< Unit of measurement override |  | ||||||
|   NumberMode mode_{NUMBER_MODE_AUTO}; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** Base-class for all numbers. | /** Base-class for all numbers. | ||||||
|  * |  * | ||||||
|  * A number can use publish_state to send out a new value. |  * A number can use publish_state to send out a new value. | ||||||
| @@ -79,7 +33,6 @@ class Number : public EntityBase { | |||||||
|   void publish_state(float state); |   void publish_state(float state); | ||||||
|  |  | ||||||
|   NumberCall make_call() { return NumberCall(this); } |   NumberCall make_call() { return NumberCall(this); } | ||||||
|   void set(float value) { make_call().set_value(value).perform(); } |  | ||||||
|  |  | ||||||
|   void add_on_state_callback(std::function<void(float)> &&callback); |   void add_on_state_callback(std::function<void(float)> &&callback); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										118
									
								
								esphome/components/number/number_call.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								esphome/components/number/number_call.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | #include "number_call.h" | ||||||
|  | #include "number.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace number { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "number"; | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::number_increment(bool cycle) { | ||||||
|  |   return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::number_decrement(bool cycle) { | ||||||
|  |   return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::with_operation(NumberOperation operation) { | ||||||
|  |   this->operation_ = operation; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::with_value(float value) { | ||||||
|  |   this->value_ = value; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | NumberCall &NumberCall::with_cycle(bool cycle) { | ||||||
|  |   this->cycle_ = cycle; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void NumberCall::perform() { | ||||||
|  |   auto *parent = this->parent_; | ||||||
|  |   const auto *name = parent->get_name().c_str(); | ||||||
|  |   const auto &traits = parent->traits; | ||||||
|  |  | ||||||
|  |   if (this->operation_ == NUMBER_OP_NONE) { | ||||||
|  |     ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   float target_value = NAN; | ||||||
|  |   float min_value = traits.get_min_value(); | ||||||
|  |   float max_value = traits.get_max_value(); | ||||||
|  |  | ||||||
|  |   if (this->operation_ == NUMBER_OP_SET) { | ||||||
|  |     ESP_LOGD(TAG, "'%s' - Setting number value", name); | ||||||
|  |     if (!this->value_.has_value() || std::isnan(*this->value_)) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     target_value = this->value_.value(); | ||||||
|  |   } else if (this->operation_ == NUMBER_OP_TO_MIN) { | ||||||
|  |     if (std::isnan(min_value)) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); | ||||||
|  |     } else { | ||||||
|  |       target_value = min_value; | ||||||
|  |     } | ||||||
|  |   } else if (this->operation_ == NUMBER_OP_TO_MAX) { | ||||||
|  |     if (std::isnan(max_value)) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); | ||||||
|  |     } else { | ||||||
|  |       target_value = max_value; | ||||||
|  |     } | ||||||
|  |   } else if (this->operation_ == NUMBER_OP_INCREMENT) { | ||||||
|  |     ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); | ||||||
|  |     if (!parent->has_state()) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     auto step = traits.get_step(); | ||||||
|  |     target_value = parent->state + (std::isnan(step) ? 1 : step); | ||||||
|  |     if (target_value > max_value) { | ||||||
|  |       if (this->cycle_ && !std::isnan(min_value)) { | ||||||
|  |         target_value = min_value; | ||||||
|  |       } else { | ||||||
|  |         target_value = max_value; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } else if (this->operation_ == NUMBER_OP_DECREMENT) { | ||||||
|  |     ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); | ||||||
|  |     if (!parent->has_state()) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     auto step = traits.get_step(); | ||||||
|  |     target_value = parent->state - (std::isnan(step) ? 1 : step); | ||||||
|  |     if (target_value < min_value) { | ||||||
|  |       if (this->cycle_ && !std::isnan(max_value)) { | ||||||
|  |         target_value = max_value; | ||||||
|  |       } else { | ||||||
|  |         target_value = min_value; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (target_value < min_value) { | ||||||
|  |     ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (target_value > max_value) { | ||||||
|  |     ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGD(TAG, "  New number value: %f", target_value); | ||||||
|  |   this->parent_->control(target_value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace number | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										43
									
								
								esphome/components/number/number_call.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/number/number_call.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "number_traits.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace number { | ||||||
|  |  | ||||||
|  | class Number; | ||||||
|  |  | ||||||
|  | enum NumberOperation { | ||||||
|  |   NUMBER_OP_NONE, | ||||||
|  |   NUMBER_OP_SET, | ||||||
|  |   NUMBER_OP_INCREMENT, | ||||||
|  |   NUMBER_OP_DECREMENT, | ||||||
|  |   NUMBER_OP_TO_MIN, | ||||||
|  |   NUMBER_OP_TO_MAX, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class NumberCall { | ||||||
|  |  public: | ||||||
|  |   explicit NumberCall(Number *parent) : parent_(parent) {} | ||||||
|  |   void perform(); | ||||||
|  |  | ||||||
|  |   NumberCall &set_value(float value); | ||||||
|  |   NumberCall &number_increment(bool cycle); | ||||||
|  |   NumberCall &number_decrement(bool cycle); | ||||||
|  |   NumberCall &number_to_min(); | ||||||
|  |   NumberCall &number_to_max(); | ||||||
|  |  | ||||||
|  |   NumberCall &with_operation(NumberOperation operation); | ||||||
|  |   NumberCall &with_value(float value); | ||||||
|  |   NumberCall &with_cycle(bool cycle); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   Number *const parent_; | ||||||
|  |   NumberOperation operation_{NUMBER_OP_NONE}; | ||||||
|  |   optional<float> value_; | ||||||
|  |   bool cycle_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace number | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										20
									
								
								esphome/components/number/number_traits.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/number/number_traits.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "number_traits.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace number { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "number"; | ||||||
|  |  | ||||||
|  | void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { | ||||||
|  |   this->unit_of_measurement_ = unit_of_measurement; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string NumberTraits::get_unit_of_measurement() { | ||||||
|  |   if (this->unit_of_measurement_.has_value()) | ||||||
|  |     return *this->unit_of_measurement_; | ||||||
|  |   return ""; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace number | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										44
									
								
								esphome/components/number/number_traits.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/number/number_traits.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace number { | ||||||
|  |  | ||||||
|  | enum NumberMode : uint8_t { | ||||||
|  |   NUMBER_MODE_AUTO = 0, | ||||||
|  |   NUMBER_MODE_BOX = 1, | ||||||
|  |   NUMBER_MODE_SLIDER = 2, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class NumberTraits { | ||||||
|  |  public: | ||||||
|  |   // Set/get the number value boundaries. | ||||||
|  |   void set_min_value(float min_value) { min_value_ = min_value; } | ||||||
|  |   float get_min_value() const { return min_value_; } | ||||||
|  |   void set_max_value(float max_value) { max_value_ = max_value; } | ||||||
|  |   float get_max_value() const { return max_value_; } | ||||||
|  |  | ||||||
|  |   // Set/get the step size for incrementing or decrementing the number value. | ||||||
|  |   void set_step(float step) { step_ = step; } | ||||||
|  |   float get_step() const { return step_; } | ||||||
|  |  | ||||||
|  |   /// Manually set the unit of measurement. | ||||||
|  |   void set_unit_of_measurement(const std::string &unit_of_measurement); | ||||||
|  |   /// Get the unit of measurement, using the manual override if set. | ||||||
|  |   std::string get_unit_of_measurement(); | ||||||
|  |  | ||||||
|  |   // Set/get the frontend mode. | ||||||
|  |   void set_mode(NumberMode mode) { this->mode_ = mode; } | ||||||
|  |   NumberMode get_mode() const { return this->mode_; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   float min_value_ = NAN; | ||||||
|  |   float max_value_ = NAN; | ||||||
|  |   float step_ = NAN; | ||||||
|  |   optional<std::string> unit_of_measurement_;  ///< Unit of measurement override | ||||||
|  |   NumberMode mode_{NUMBER_MODE_AUTO}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace number | ||||||
|  | }  // namespace esphome | ||||||
| @@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens | |||||||
|  |  | ||||||
| void PMSX003Component::loop() { | void PMSX003Component::loop() { | ||||||
|   const uint32_t now = millis(); |   const uint32_t now = millis(); | ||||||
|  |  | ||||||
|  |   // If we update less often than it takes the device to stabilise, spin the fan down | ||||||
|  |   // rather than running it constantly. It does take some time to stabilise, so we | ||||||
|  |   // need to keep track of what state we're in. | ||||||
|  |   if (this->update_interval_ > PMS_STABILISING_MS) { | ||||||
|  |     if (this->initialised_ == 0) { | ||||||
|  |       this->send_command_(PMS_CMD_AUTO_MANUAL, 0); | ||||||
|  |       this->send_command_(PMS_CMD_ON_STANDBY, 1); | ||||||
|  |       this->initialised_ = 1; | ||||||
|  |     } | ||||||
|  |     switch (this->state_) { | ||||||
|  |       case PMSX003_STATE_IDLE: | ||||||
|  |         // Power on the sensor now so it'll be ready when we hit the update time | ||||||
|  |         if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS)) | ||||||
|  |           return; | ||||||
|  |  | ||||||
|  |         this->state_ = PMSX003_STATE_STABILISING; | ||||||
|  |         this->send_command_(PMS_CMD_ON_STANDBY, 1); | ||||||
|  |         this->fan_on_time_ = now; | ||||||
|  |         return; | ||||||
|  |       case PMSX003_STATE_STABILISING: | ||||||
|  |         // wait for the sensor to be stable | ||||||
|  |         if (now - this->fan_on_time_ < PMS_STABILISING_MS) | ||||||
|  |           return; | ||||||
|  |         // consume any command responses that are in the serial buffer | ||||||
|  |         while (this->available()) | ||||||
|  |           this->read_byte(&this->data_[0]); | ||||||
|  |         // Trigger a new read | ||||||
|  |         this->send_command_(PMS_CMD_TRIG_MANUAL, 0); | ||||||
|  |         this->state_ = PMSX003_STATE_WAITING; | ||||||
|  |         break; | ||||||
|  |       case PMSX003_STATE_WAITING: | ||||||
|  |         // Just go ahead and read stuff | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } else if (now - this->last_update_ < this->update_interval_) { | ||||||
|  |     // Otherwise just leave the sensor powered up and come back when we hit the update | ||||||
|  |     // time | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (now - this->last_transmission_ >= 500) { |   if (now - this->last_transmission_ >= 500) { | ||||||
|     // last transmission too long ago. Reset RX index. |     // last transmission too long ago. Reset RX index. | ||||||
|     this->data_index_ = 0; |     this->data_index_ = 0; | ||||||
| @@ -65,6 +106,7 @@ void PMSX003Component::loop() { | |||||||
|       // finished |       // finished | ||||||
|       this->parse_data_(); |       this->parse_data_(); | ||||||
|       this->data_index_ = 0; |       this->data_index_ = 0; | ||||||
|  |       this->last_update_ = now; | ||||||
|     } else if (!*check) { |     } else if (!*check) { | ||||||
|       // wrong data |       // wrong data | ||||||
|       this->data_index_ = 0; |       this->data_index_ = 0; | ||||||
| @@ -131,6 +173,25 @@ optional<bool> PMSX003Component::check_byte_() { | |||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) { | ||||||
|  |   this->data_index_ = 0; | ||||||
|  |   this->data_[data_index_++] = 0x42; | ||||||
|  |   this->data_[data_index_++] = 0x4D; | ||||||
|  |   this->data_[data_index_++] = cmd; | ||||||
|  |   this->data_[data_index_++] = (data >> 8) & 0xFF; | ||||||
|  |   this->data_[data_index_++] = (data >> 0) & 0xFF; | ||||||
|  |   int sum = 0; | ||||||
|  |   for (int i = 0; i < data_index_; i++) { | ||||||
|  |     sum += this->data_[i]; | ||||||
|  |   } | ||||||
|  |   this->data_[data_index_++] = (sum >> 8) & 0xFF; | ||||||
|  |   this->data_[data_index_++] = (sum >> 0) & 0xFF; | ||||||
|  |   for (int i = 0; i < data_index_; i++) { | ||||||
|  |     this->write_byte(this->data_[i]); | ||||||
|  |   } | ||||||
|  |   this->data_index_ = 0; | ||||||
|  | } | ||||||
|  |  | ||||||
| void PMSX003Component::parse_data_() { | void PMSX003Component::parse_data_() { | ||||||
|   switch (this->type_) { |   switch (this->type_) { | ||||||
|     case PMSX003_TYPE_5003ST: { |     case PMSX003_TYPE_5003ST: { | ||||||
| @@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Spin down the sensor again if we aren't going to need it until more time has | ||||||
|  |   // passed than it takes to stabilise | ||||||
|  |   if (this->update_interval_ > PMS_STABILISING_MS) { | ||||||
|  |     this->send_command_(PMS_CMD_ON_STANDBY, 0); | ||||||
|  |     this->state_ = PMSX003_STATE_IDLE; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   this->status_clear_warning(); |   this->status_clear_warning(); | ||||||
| } | } | ||||||
| uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { | uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { | ||||||
|   | |||||||
| @@ -7,6 +7,13 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace pmsx003 { | namespace pmsx003 { | ||||||
|  |  | ||||||
|  | // known command bytes | ||||||
|  | #define PMS_CMD_AUTO_MANUAL 0xE1  // data=0: perform measurement manually, data=1: perform measurement automatically | ||||||
|  | #define PMS_CMD_TRIG_MANUAL 0xE2  // trigger a manual measurement | ||||||
|  | #define PMS_CMD_ON_STANDBY 0xE4   // data=0: go to standby mode, data=1: go to normal mode | ||||||
|  |  | ||||||
|  | static const uint16_t PMS_STABILISING_MS = 30000;  // time taken for the sensor to become stable after power on | ||||||
|  |  | ||||||
| enum PMSX003Type { | enum PMSX003Type { | ||||||
|   PMSX003_TYPE_X003 = 0, |   PMSX003_TYPE_X003 = 0, | ||||||
|   PMSX003_TYPE_5003T, |   PMSX003_TYPE_5003T, | ||||||
| @@ -14,6 +21,12 @@ enum PMSX003Type { | |||||||
|   PMSX003_TYPE_5003S, |   PMSX003_TYPE_5003S, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum PMSX003State { | ||||||
|  |   PMSX003_STATE_IDLE = 0, | ||||||
|  |   PMSX003_STATE_STABILISING, | ||||||
|  |   PMSX003_STATE_WAITING, | ||||||
|  | }; | ||||||
|  |  | ||||||
| class PMSX003Component : public uart::UARTDevice, public Component { | class PMSX003Component : public uart::UARTDevice, public Component { | ||||||
|  public: |  public: | ||||||
|   PMSX003Component() = default; |   PMSX003Component() = default; | ||||||
| @@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component { | |||||||
|  |  | ||||||
|   void set_type(PMSX003Type type) { type_ = type; } |   void set_type(PMSX003Type type) { type_ = type; } | ||||||
|  |  | ||||||
|  |   void set_update_interval(uint32_t val) { update_interval_ = val; }; | ||||||
|  |  | ||||||
|   void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor); |   void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor); | ||||||
|   void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor); |   void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor); | ||||||
|   void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); |   void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); | ||||||
| @@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component { | |||||||
|  protected: |  protected: | ||||||
|   optional<bool> check_byte_(); |   optional<bool> check_byte_(); | ||||||
|   void parse_data_(); |   void parse_data_(); | ||||||
|  |   void send_command_(uint8_t cmd, uint16_t data); | ||||||
|   uint16_t get_16_bit_uint_(uint8_t start_index); |   uint16_t get_16_bit_uint_(uint8_t start_index); | ||||||
|  |  | ||||||
|   uint8_t data_[64]; |   uint8_t data_[64]; | ||||||
|   uint8_t data_index_{0}; |   uint8_t data_index_{0}; | ||||||
|  |   uint8_t initialised_{0}; | ||||||
|  |   uint32_t fan_on_time_{0}; | ||||||
|  |   uint32_t last_update_{0}; | ||||||
|   uint32_t last_transmission_{0}; |   uint32_t last_transmission_{0}; | ||||||
|  |   uint32_t update_interval_{0}; | ||||||
|  |   PMSX003State state_{PMSX003_STATE_IDLE}; | ||||||
|   PMSX003Type type_; |   PMSX003Type type_; | ||||||
|  |  | ||||||
|   // "Standard Particle" |   // "Standard Particle" | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import sensor, uart | from esphome.components import sensor, uart | ||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_FORMALDEHYDE, |     CONF_FORMALDEHYDE, | ||||||
|     CONF_HUMIDITY, |     CONF_HUMIDITY, | ||||||
| @@ -17,6 +18,7 @@ from esphome.const import ( | |||||||
|     CONF_PM_2_5UM, |     CONF_PM_2_5UM, | ||||||
|     CONF_PM_5_0UM, |     CONF_PM_5_0UM, | ||||||
|     CONF_PM_10_0UM, |     CONF_PM_10_0UM, | ||||||
|  |     CONF_UPDATE_INTERVAL, | ||||||
|     CONF_TEMPERATURE, |     CONF_TEMPERATURE, | ||||||
|     CONF_TYPE, |     CONF_TYPE, | ||||||
|     DEVICE_CLASS_PM1, |     DEVICE_CLASS_PM1, | ||||||
| @@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST" | |||||||
| TYPE_PMS5003S = "PMS5003S" | TYPE_PMS5003S = "PMS5003S" | ||||||
|  |  | ||||||
| PMSX003Type = pmsx003_ns.enum("PMSX003Type") | PMSX003Type = pmsx003_ns.enum("PMSX003Type") | ||||||
|  |  | ||||||
| PMSX003_TYPES = { | PMSX003_TYPES = { | ||||||
|     TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, |     TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, | ||||||
|     TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, |     TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, | ||||||
| @@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value): | |||||||
|     return value |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_update_interval(value): | ||||||
|  |     value = cv.positive_time_period_milliseconds(value) | ||||||
|  |     if value == cv.time_period("0s"): | ||||||
|  |         return value | ||||||
|  |     if value < cv.time_period("30s"): | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             "Update interval must be greater than or equal to 30 seconds if set." | ||||||
|  |         ) | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -157,6 +171,7 @@ CONFIG_SCHEMA = ( | |||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval, | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.COMPONENT_SCHEMA) |     .extend(cv.COMPONENT_SCHEMA) | ||||||
| @@ -164,6 +179,17 @@ CONFIG_SCHEMA = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def final_validate(config): | ||||||
|  |     require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s") | ||||||
|  |     schema = uart.final_validate_device_schema( | ||||||
|  |         "pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx | ||||||
|  |     ) | ||||||
|  |     schema(config) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FINAL_VALIDATE_SCHEMA = final_validate | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
| @@ -230,3 +256,5 @@ async def to_code(config): | |||||||
|     if CONF_FORMALDEHYDE in config: |     if CONF_FORMALDEHYDE in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE]) |         sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE]) | ||||||
|         cg.add(var.set_formaldehyde_sensor(sens)) |         cg.add(var.set_formaldehyde_sensor(sens)) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								esphome/components/scd4x/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/scd4x/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "scd4x.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scd4x { | ||||||
|  |  | ||||||
|  | template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts...>, public Parented<SCD4XComponent> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     if (this->value_.has_value()) { | ||||||
|  |       this->parent_->perform_forced_calibration(value_.value()); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   TEMPLATABLE_VALUE(uint16_t, value) | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class FactoryResetAction : public Action<Ts...>, public Parented<SCD4XComponent> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->factory_reset(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scd4x | ||||||
|  | }  // namespace esphome | ||||||
| @@ -13,29 +13,24 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; | |||||||
| static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000; | static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000; | ||||||
| static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416; | static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416; | ||||||
| static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1; | static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1; | ||||||
|  | static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac; | ||||||
|  | static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d;  // SCD41 only | ||||||
|  | static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196; | ||||||
| static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8; | static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8; | ||||||
| static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05; | static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05; | ||||||
| static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f; | static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f; | ||||||
| static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; | static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; | ||||||
|  | static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; | ||||||
|  | static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; | ||||||
| static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; | static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; | ||||||
|  | static const uint16_t SCD41_ID = 0x1408; | ||||||
|  | static const uint16_t SCD40_ID = 0x440; | ||||||
|  |  | ||||||
| void SCD4XComponent::setup() { | void SCD4XComponent::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up scd4x..."); |   ESP_LOGCONFIG(TAG, "Setting up scd4x..."); | ||||||
|  |  | ||||||
|   // the sensor needs 1000 ms to enter the idle state |   // the sensor needs 1000 ms to enter the idle state | ||||||
|   this->set_timeout(1000, [this]() { |   this->set_timeout(1000, [this]() { | ||||||
|     uint16_t raw_read_status; |     this->status_clear_error(); | ||||||
|     if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) { |  | ||||||
|       ESP_LOGE(TAG, "Failed to read data ready status"); |  | ||||||
|       this->mark_failed(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     uint32_t stop_measurement_delay = 0; |  | ||||||
|     // In order to query the device periodic measurement must be ceased |  | ||||||
|     if (raw_read_status) { |  | ||||||
|       ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); |  | ||||||
|     if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { |     if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { | ||||||
|       ESP_LOGE(TAG, "Failed to stop measurements"); |       ESP_LOGE(TAG, "Failed to stop measurements"); | ||||||
|       this->mark_failed(); |       this->mark_failed(); | ||||||
| @@ -43,9 +38,7 @@ void SCD4XComponent::setup() { | |||||||
|     } |     } | ||||||
|     // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after |     // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after | ||||||
|     // issuing the stop_periodic_measurement command |     // issuing the stop_periodic_measurement command | ||||||
|       stop_measurement_delay = 500; |     this->set_timeout(500, [this]() { | ||||||
|     } |  | ||||||
|     this->set_timeout(stop_measurement_delay, [this]() { |  | ||||||
|       uint16_t raw_serial_number[3]; |       uint16_t raw_serial_number[3]; | ||||||
|       if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) { |       if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) { | ||||||
|         ESP_LOGE(TAG, "Failed to read serial number"); |         ESP_LOGE(TAG, "Failed to read serial number"); | ||||||
| @@ -89,15 +82,9 @@ void SCD4XComponent::setup() { | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Finally start sensor measurements |  | ||||||
|       if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { |  | ||||||
|         ESP_LOGE(TAG, "Error starting continuous measurements."); |  | ||||||
|         this->error_code_ = MEASUREMENT_INIT_FAILED; |  | ||||||
|         this->mark_failed(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       initialized_ = true; |       initialized_ = true; | ||||||
|  |       // Finally start sensor measurements | ||||||
|  |       this->start_measurement_(); | ||||||
|       ESP_LOGD(TAG, "Sensor initialized"); |       ESP_LOGD(TAG, "Sensor initialized"); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @@ -123,6 +110,10 @@ void SCD4XComponent::dump_config() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   ESP_LOGCONFIG(TAG, "  Automatic self calibration: %s", ONOFF(this->enable_asc_)); |   ESP_LOGCONFIG(TAG, "  Automatic self calibration: %s", ONOFF(this->enable_asc_)); | ||||||
|  |   if (this->ambient_pressure_source_ != nullptr) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using sensor '%s'", | ||||||
|  |                   this->ambient_pressure_source_->get_name().c_str()); | ||||||
|  |   } else { | ||||||
|     if (this->ambient_pressure_compensation_) { |     if (this->ambient_pressure_compensation_) { | ||||||
|       ESP_LOGCONFIG(TAG, "  Altitude compensation disabled"); |       ESP_LOGCONFIG(TAG, "  Altitude compensation disabled"); | ||||||
|       ESP_LOGCONFIG(TAG, "  Ambient pressure compensation: %dmBar", this->ambient_pressure_); |       ESP_LOGCONFIG(TAG, "  Ambient pressure compensation: %dmBar", this->ambient_pressure_); | ||||||
| @@ -130,6 +121,21 @@ void SCD4XComponent::dump_config() { | |||||||
|       ESP_LOGCONFIG(TAG, "  Ambient pressure compensation disabled"); |       ESP_LOGCONFIG(TAG, "  Ambient pressure compensation disabled"); | ||||||
|       ESP_LOGCONFIG(TAG, "  Altitude compensation: %dm", this->altitude_compensation_); |       ESP_LOGCONFIG(TAG, "  Altitude compensation: %dm", this->altitude_compensation_); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |   switch (this->measurement_mode_) { | ||||||
|  |     case PERIODIC: | ||||||
|  |       ESP_LOGCONFIG(TAG, "  Measurement mode: periodic (5s)"); | ||||||
|  |       break; | ||||||
|  |     case LOW_POWER_PERIODIC: | ||||||
|  |       ESP_LOGCONFIG(TAG, "  Measurement mode: low power periodic (30s)"); | ||||||
|  |       break; | ||||||
|  |     case SINGLE_SHOT: | ||||||
|  |       ESP_LOGCONFIG(TAG, "  Measurement mode: single shot"); | ||||||
|  |       break; | ||||||
|  |     case SINGLE_SHOT_RHT_ONLY: | ||||||
|  |       ESP_LOGCONFIG(TAG, "  Measurement mode: single shot rht only"); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|   ESP_LOGCONFIG(TAG, "  Temperature offset: %.2f °C", this->temperature_offset_); |   ESP_LOGCONFIG(TAG, "  Temperature offset: %.2f °C", this->temperature_offset_); | ||||||
|   LOG_UPDATE_INTERVAL(this); |   LOG_UPDATE_INTERVAL(this); | ||||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); |   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||||
| @@ -149,6 +155,13 @@ void SCD4XComponent::update() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   uint32_t wait_time = 0; | ||||||
|  |   if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { | ||||||
|  |     start_measurement_(); | ||||||
|  |     wait_time = | ||||||
|  |         this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50;  // Single shot measurement takes 5 secs rht mode 50 ms | ||||||
|  |   } | ||||||
|  |   this->set_timeout(wait_time, [this]() { | ||||||
|     // Check if data is ready |     // Check if data is ready | ||||||
|     if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { |     if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
| @@ -156,6 +169,7 @@ void SCD4XComponent::update() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     uint16_t raw_read_status; |     uint16_t raw_read_status; | ||||||
|  |  | ||||||
|     if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { |     if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|       ESP_LOGW(TAG, "Data not ready yet!"); |       ESP_LOGW(TAG, "Data not ready yet!"); | ||||||
| @@ -165,16 +179,14 @@ void SCD4XComponent::update() { | |||||||
|     if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { |     if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { | ||||||
|       ESP_LOGW(TAG, "Error reading measurement!"); |       ESP_LOGW(TAG, "Error reading measurement!"); | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|     return; |       return;  // NO RETRY | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Read off sensor data |     // Read off sensor data | ||||||
|     uint16_t raw_data[3]; |     uint16_t raw_data[3]; | ||||||
|     if (!this->read_data(raw_data, 3)) { |     if (!this->read_data(raw_data, 3)) { | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (this->co2_sensor_ != nullptr) |     if (this->co2_sensor_ != nullptr) | ||||||
|       this->co2_sensor_->publish_state(raw_data[0]); |       this->co2_sensor_->publish_state(raw_data[0]); | ||||||
|  |  | ||||||
| @@ -182,14 +194,66 @@ void SCD4XComponent::update() { | |||||||
|       const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); |       const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); | ||||||
|       this->temperature_sensor_->publish_state(temperature); |       this->temperature_sensor_->publish_state(temperature); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (this->humidity_sensor_ != nullptr) { |     if (this->humidity_sensor_ != nullptr) { | ||||||
|       const float humidity = (100.0f * raw_data[2]) / (1 << 16); |       const float humidity = (100.0f * raw_data[2]) / (1 << 16); | ||||||
|       this->humidity_sensor_->publish_state(humidity); |       this->humidity_sensor_->publish_state(humidity); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this->status_clear_warning(); |     this->status_clear_warning(); | ||||||
|  |   });  // set_timeout | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) { | ||||||
|  |   /* | ||||||
|  |     Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power | ||||||
|  |     periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2 | ||||||
|  |     concentration before performing a forced recalibration. | ||||||
|  |   */ | ||||||
|  |   if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to stop measurements"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |   } | ||||||
|  |   this->set_timeout(500, [this, current_co2_concentration]() { | ||||||
|  |     if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { | ||||||
|  |       ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); | ||||||
|  |       // frc takes 400 ms | ||||||
|  |       // because this method will be used very rarly | ||||||
|  |       // the simple aproach with delay is ok | ||||||
|  |       delay(400);  // NOLINT' | ||||||
|  |       if (!this->start_measurement_()) { | ||||||
|  |         return false; | ||||||
|  |       } else { | ||||||
|  |         ESP_LOGD(TAG, "forced calibration complete"); | ||||||
|  |       } | ||||||
|  |       return true; | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "force calibration failed"); | ||||||
|  |       this->error_code_ = FRC_FAILED; | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SCD4XComponent::factory_reset() { | ||||||
|  |   if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to stop measurements"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->set_timeout(500, [this]() { | ||||||
|  |     if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to send factory reset command"); | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     ESP_LOGD(TAG, "Factory reset complete"); | ||||||
|  |     return true; | ||||||
|  |   }); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
| // Note pressure in bar here. Convert to hPa | // Note pressure in bar here. Convert to hPa | ||||||
| void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { | void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { | ||||||
|   ambient_pressure_compensation_ = true; |   ambient_pressure_compensation_ = true; | ||||||
| @@ -213,5 +277,38 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool SCD4XComponent::start_measurement_() { | ||||||
|  |   uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; | ||||||
|  |   switch (this->measurement_mode_) { | ||||||
|  |     case PERIODIC: | ||||||
|  |       measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; | ||||||
|  |       break; | ||||||
|  |     case LOW_POWER_PERIODIC: | ||||||
|  |       measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS; | ||||||
|  |       break; | ||||||
|  |     case SINGLE_SHOT: | ||||||
|  |       measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT; | ||||||
|  |       break; | ||||||
|  |     case SINGLE_SHOT_RHT_ONLY: | ||||||
|  |       measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY; | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static uint8_t remaining_retries = 3; | ||||||
|  |   while (remaining_retries) { | ||||||
|  |     if (!this->write_command(measurement_command)) { | ||||||
|  |       ESP_LOGE(TAG, "Error starting measurements."); | ||||||
|  |       this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       if (--remaining_retries == 0) | ||||||
|  |         return false; | ||||||
|  |       delay(50);  // NOLINT wait 50 ms and try again | ||||||
|  |     } | ||||||
|  |     this->status_clear_warning(); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace scd4x | }  // namespace scd4x | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  | #include <vector> | ||||||
|  | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
| #include "esphome/components/sensirion_common/i2c_sensirion.h" | #include "esphome/components/sensirion_common/i2c_sensirion.h" | ||||||
| @@ -7,7 +8,14 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace scd4x { | namespace scd4x { | ||||||
|  |  | ||||||
| enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; | enum ERRORCODE { | ||||||
|  |   COMMUNICATION_FAILED, | ||||||
|  |   SERIAL_NUMBER_IDENTIFICATION_FAILED, | ||||||
|  |   MEASUREMENT_INIT_FAILED, | ||||||
|  |   FRC_FAILED, | ||||||
|  |   UNKNOWN | ||||||
|  | }; | ||||||
|  | enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY }; | ||||||
|  |  | ||||||
| class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { | class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { | ||||||
|  public: |  public: | ||||||
| @@ -25,10 +33,13 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri | |||||||
|   void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } |   void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||||
|   void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }; |   void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }; | ||||||
|   void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } |   void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||||
|  |   void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } | ||||||
|  |   bool perform_forced_calibration(uint16_t current_co2_concentration); | ||||||
|  |   bool factory_reset(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); |   bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); | ||||||
|  |   bool start_measurement_(); | ||||||
|   ERRORCODE error_code_; |   ERRORCODE error_code_; | ||||||
|  |  | ||||||
|   bool initialized_{false}; |   bool initialized_{false}; | ||||||
| @@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri | |||||||
|   bool ambient_pressure_compensation_; |   bool ambient_pressure_compensation_; | ||||||
|   uint16_t ambient_pressure_; |   uint16_t ambient_pressure_; | ||||||
|   bool enable_asc_; |   bool enable_asc_; | ||||||
|  |   MeasurementMode measurement_mode_{PERIODIC}; | ||||||
|   sensor::Sensor *co2_sensor_{nullptr}; |   sensor::Sensor *co2_sensor_{nullptr}; | ||||||
|   sensor::Sensor *temperature_sensor_{nullptr}; |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|   sensor::Sensor *humidity_sensor_{nullptr}; |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|   | |||||||
| @@ -2,11 +2,15 @@ import esphome.codegen as cg | |||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import i2c, sensor | from esphome.components import i2c, sensor | ||||||
| from esphome.components import sensirion_common | from esphome.components import sensirion_common | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.automation import maybe_simple_id | ||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_CO2, |     CONF_CO2, | ||||||
|     CONF_HUMIDITY, |     CONF_HUMIDITY, | ||||||
|     CONF_TEMPERATURE, |     CONF_TEMPERATURE, | ||||||
|  |     CONF_VALUE, | ||||||
|     DEVICE_CLASS_CARBON_DIOXIDE, |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|     DEVICE_CLASS_HUMIDITY, |     DEVICE_CLASS_HUMIDITY, | ||||||
|     DEVICE_CLASS_TEMPERATURE, |     DEVICE_CLASS_TEMPERATURE, | ||||||
| @@ -19,7 +23,7 @@ from esphome.const import ( | |||||||
|     UNIT_PERCENT, |     UNIT_PERCENT, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| CODEOWNERS = ["@sjtrny"] | CODEOWNERS = ["@sjtrny", "@martgras"] | ||||||
| DEPENDENCIES = ["i2c"] | DEPENDENCIES = ["i2c"] | ||||||
| AUTO_LOAD = ["sensirion_common"] | AUTO_LOAD = ["sensirion_common"] | ||||||
|  |  | ||||||
| @@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x") | |||||||
| SCD4XComponent = scd4x_ns.class_( | SCD4XComponent = scd4x_ns.class_( | ||||||
|     "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice |     "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice | ||||||
| ) | ) | ||||||
|  | MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE") | ||||||
|  | MEASUREMENT_MODE_OPTIONS = { | ||||||
|  |     "periodic": MeasurementMode.PERIODIC, | ||||||
|  |     "low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC, | ||||||
|  |     "single_shot": MeasurementMode.SINGLE_SHOT, | ||||||
|  |     "single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Actions | ||||||
|  | PerformForcedCalibrationAction = scd4x_ns.class_( | ||||||
|  |     "PerformForcedCalibrationAction", automation.Action | ||||||
|  | ) | ||||||
|  | FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" |  | ||||||
| CONF_ALTITUDE_COMPENSATION = "altitude_compensation" | CONF_ALTITUDE_COMPENSATION = "altitude_compensation" | ||||||
| CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" | CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" | ||||||
| CONF_TEMPERATURE_OFFSET = "temperature_offset" |  | ||||||
| CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" | CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" | ||||||
|  | CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" | ||||||
|  | CONF_MEASUREMENT_MODE = "measurement_mode" | ||||||
|  | CONF_TEMPERATURE_OFFSET = "temperature_offset" | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
| @@ -69,6 +90,9 @@ CONFIG_SCHEMA = ( | |||||||
|             cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( |             cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( | ||||||
|                 sensor.Sensor |                 sensor.Sensor | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum( | ||||||
|  |                 MEASUREMENT_MODE_OPTIONS, lower=True | ||||||
|  |             ), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.polling_component_schema("60s")) |     .extend(cv.polling_component_schema("60s")) | ||||||
| @@ -106,3 +130,42 @@ async def to_code(config): | |||||||
|     if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: |     if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: | ||||||
|         sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) |         sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) | ||||||
|         cg.add(var.set_ambient_pressure_source(sens)) |         cg.add(var.set_ambient_pressure_source(sens)) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE])) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SCD4X_ACTION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.use_id(SCD4XComponent), | ||||||
|  |         cv.Required(CONF_VALUE): cv.templatable(cv.positive_int), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "scd4x.perform_forced_calibration", | ||||||
|  |     PerformForcedCalibrationAction, | ||||||
|  |     SCD4X_ACTION_SCHEMA, | ||||||
|  | ) | ||||||
|  | async def scd4x_frc_to_code(config, action_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) | ||||||
|  |     cg.add(var.set_value(template_)) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(SCD4XComponent), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def scd4x_reset_to_code(config, action_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |     await cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     return var | ||||||
|   | |||||||
| @@ -9,6 +9,10 @@ from esphome.const import ( | |||||||
|     CONF_OPTION, |     CONF_OPTION, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|     CONF_MQTT_ID, |     CONF_MQTT_ID, | ||||||
|  |     CONF_CYCLE, | ||||||
|  |     CONF_MODE, | ||||||
|  |     CONF_OPERATION, | ||||||
|  |     CONF_INDEX, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
| from esphome.cpp_helpers import setup_entity | from esphome.cpp_helpers import setup_entity | ||||||
| @@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr") | |||||||
|  |  | ||||||
| # Triggers | # Triggers | ||||||
| SelectStateTrigger = select_ns.class_( | SelectStateTrigger = select_ns.class_( | ||||||
|     "SelectStateTrigger", automation.Trigger.template(cg.float_) |     "SelectStateTrigger", | ||||||
|  |     automation.Trigger.template(cg.std_string, cg.size_t), | ||||||
| ) | ) | ||||||
|  |  | ||||||
| # Actions | # Actions | ||||||
| SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) | SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) | ||||||
|  | SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action) | ||||||
|  | SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action) | ||||||
|  |  | ||||||
|  | # Enums | ||||||
|  | SelectOperation = select_ns.enum("SelectOperation") | ||||||
|  | SELECT_OPERATION_OPTIONS = { | ||||||
|  |     "NEXT": SelectOperation.SELECT_OP_NEXT, | ||||||
|  |     "PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS, | ||||||
|  |     "FIRST": SelectOperation.SELECT_OP_FIRST, | ||||||
|  |     "LAST": SelectOperation.SELECT_OP_LAST, | ||||||
|  | } | ||||||
|  |  | ||||||
| icon = cv.icon | icon = cv.icon | ||||||
|  |  | ||||||
|  |  | ||||||
| SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | ||||||
|     { |     { | ||||||
|         cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), |         cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), | ||||||
| @@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]): | |||||||
|  |  | ||||||
|     for conf in config.get(CONF_ON_VALUE, []): |     for conf in config.get(CONF_ON_VALUE, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|         await automation.build_automation(trigger, [(cg.std_string, "x")], conf) |         await automation.build_automation( | ||||||
|  |             trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     if CONF_MQTT_ID in config: |     if CONF_MQTT_ID in config: | ||||||
|         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) |         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) | ||||||
| @@ -76,12 +95,18 @@ async def to_code(config): | |||||||
|     cg.add_global(select_ns.using) |     cg.add_global(select_ns.using) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | OPERATION_BASE_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(Select), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @automation.register_action( | @automation.register_action( | ||||||
|     "select.set", |     "select.set", | ||||||
|     SelectSetAction, |     SelectSetAction, | ||||||
|     cv.Schema( |     OPERATION_BASE_SCHEMA.extend( | ||||||
|         { |         { | ||||||
|             cv.Required(CONF_ID): cv.use_id(Select), |  | ||||||
|             cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), |             cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), | ||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
| @@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args): | |||||||
|     template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string) |     template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string) | ||||||
|     cg.add(var.set_option(template_)) |     cg.add(var.set_option(template_)) | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "select.set_index", | ||||||
|  |     SelectSetIndexAction, | ||||||
|  |     OPERATION_BASE_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_INDEX): cv.templatable(cv.positive_int), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | async def select_set_index_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_INDEX], args, cg.size_t) | ||||||
|  |     cg.add(var.set_index(template_)) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "select.operation", | ||||||
|  |     SelectOperationAction, | ||||||
|  |     OPERATION_BASE_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_OPERATION): cv.templatable( | ||||||
|  |                 cv.enum(SELECT_OPERATION_OPTIONS, upper=True) | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "select.next", | ||||||
|  |     SelectOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True), | ||||||
|  |                 cv.Optional(CONF_CYCLE, default=True): cv.boolean, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "select.previous", | ||||||
|  |     SelectOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of( | ||||||
|  |                     "PREVIOUS", upper=True | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional(CONF_CYCLE, default=True): cv.boolean, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "select.first", | ||||||
|  |     SelectOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_action( | ||||||
|  |     "select.last", | ||||||
|  |     SelectOperationAction, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         OPERATION_BASE_SCHEMA.extend( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | async def select_operation_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) | ||||||
|  |     if CONF_OPERATION in config: | ||||||
|  |         op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation) | ||||||
|  |         cg.add(var.set_operation(op_)) | ||||||
|  |         if CONF_CYCLE in config: | ||||||
|  |             cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) | ||||||
|  |             cg.add(var.set_cycle(cycle_)) | ||||||
|  |     if CONF_MODE in config: | ||||||
|  |         cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]])) | ||||||
|  |         if CONF_CYCLE in config: | ||||||
|  |             cg.add(var.set_cycle(config[CONF_CYCLE])) | ||||||
|  |     return var | ||||||
|   | |||||||
| @@ -7,16 +7,16 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace select { | namespace select { | ||||||
|  |  | ||||||
| class SelectStateTrigger : public Trigger<std::string> { | class SelectStateTrigger : public Trigger<std::string, size_t> { | ||||||
|  public: |  public: | ||||||
|   explicit SelectStateTrigger(Select *parent) { |   explicit SelectStateTrigger(Select *parent) { | ||||||
|     parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); |     parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); }); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| template<typename... Ts> class SelectSetAction : public Action<Ts...> { | template<typename... Ts> class SelectSetAction : public Action<Ts...> { | ||||||
|  public: |  public: | ||||||
|   SelectSetAction(Select *select) : select_(select) {} |   explicit SelectSetAction(Select *select) : select_(select) {} | ||||||
|   TEMPLATABLE_VALUE(std::string, option) |   TEMPLATABLE_VALUE(std::string, option) | ||||||
|  |  | ||||||
|   void play(Ts... x) override { |   void play(Ts... x) override { | ||||||
| @@ -29,5 +29,39 @@ template<typename... Ts> class SelectSetAction : public Action<Ts...> { | |||||||
|   Select *select_; |   Select *select_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SelectSetIndexAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit SelectSetIndexAction(Select *select) : select_(select) {} | ||||||
|  |   TEMPLATABLE_VALUE(size_t, index) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     auto call = this->select_->make_call(); | ||||||
|  |     call.set_index(this->index_.value(x...)); | ||||||
|  |     call.perform(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   Select *select_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SelectOperationAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit SelectOperationAction(Select *select) : select_(select) {} | ||||||
|  |   TEMPLATABLE_VALUE(bool, cycle) | ||||||
|  |   TEMPLATABLE_VALUE(SelectOperation, operation) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     auto call = this->select_->make_call(); | ||||||
|  |     call.with_operation(this->operation_.value(x...)); | ||||||
|  |     if (this->cycle_.has_value()) { | ||||||
|  |       call.with_cycle(this->cycle_.value(x...)); | ||||||
|  |     } | ||||||
|  |     call.perform(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   Select *select_; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace select | }  // namespace select | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -6,37 +6,58 @@ namespace select { | |||||||
|  |  | ||||||
| static const char *const TAG = "select"; | static const char *const TAG = "select"; | ||||||
|  |  | ||||||
| void SelectCall::perform() { |  | ||||||
|   ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); |  | ||||||
|   if (!this->option_.has_value()) { |  | ||||||
|     ESP_LOGW(TAG, "No value set for SelectCall"); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const auto &traits = this->parent_->traits; |  | ||||||
|   auto value = *this->option_; |  | ||||||
|   auto options = traits.get_options(); |  | ||||||
|  |  | ||||||
|   if (std::find(options.begin(), options.end(), value) == options.end()) { |  | ||||||
|     ESP_LOGW(TAG, "  Option %s is not a valid option.", value.c_str()); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ESP_LOGD(TAG, "  Option: %s", (*this->option_).c_str()); |  | ||||||
|   this->parent_->control(*this->option_); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void Select::publish_state(const std::string &state) { | void Select::publish_state(const std::string &state) { | ||||||
|  |   auto index = this->index_of(state); | ||||||
|  |   const auto *name = this->get_name().c_str(); | ||||||
|  |   if (index.has_value()) { | ||||||
|     this->has_state_ = true; |     this->has_state_ = true; | ||||||
|     this->state = state; |     this->state = state; | ||||||
|   ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); |     ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value()); | ||||||
|   this->state_callback_.call(state); |     this->state_callback_.call(state, index.value()); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void Select::add_on_state_callback(std::function<void(std::string)> &&callback) { | void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) { | ||||||
|   this->state_callback_.add(std::move(callback)); |   this->state_callback_.add(std::move(callback)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } | ||||||
|  |  | ||||||
|  | bool Select::has_index(size_t index) const { return index < this->size(); } | ||||||
|  |  | ||||||
|  | size_t Select::size() const { | ||||||
|  |   auto options = traits.get_options(); | ||||||
|  |   return options.size(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | optional<size_t> Select::index_of(const std::string &option) const { | ||||||
|  |   auto options = traits.get_options(); | ||||||
|  |   auto it = std::find(options.begin(), options.end(), option); | ||||||
|  |   if (it == options.end()) { | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  |   return std::distance(options.begin(), it); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | optional<size_t> Select::active_index() const { | ||||||
|  |   if (this->has_state()) { | ||||||
|  |     return this->index_of(this->state); | ||||||
|  |   } else { | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | optional<std::string> Select::at(size_t index) const { | ||||||
|  |   if (this->has_index(index)) { | ||||||
|  |     auto options = traits.get_options(); | ||||||
|  |     return options.at(index); | ||||||
|  |   } else { | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| uint32_t Select::hash_base() { return 2812997003UL; } | uint32_t Select::hash_base() { return 2812997003UL; } | ||||||
|  |  | ||||||
| }  // namespace select | }  // namespace select | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <set> |  | ||||||
| #include <utility> |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/entity_base.h" | #include "esphome/core/entity_base.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  | #include "select_call.h" | ||||||
|  | #include "select_traits.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace select { | namespace select { | ||||||
| @@ -17,33 +17,6 @@ namespace select { | |||||||
|     } \ |     } \ | ||||||
|   } |   } | ||||||
|  |  | ||||||
| class Select; |  | ||||||
|  |  | ||||||
| class SelectCall { |  | ||||||
|  public: |  | ||||||
|   explicit SelectCall(Select *parent) : parent_(parent) {} |  | ||||||
|   void perform(); |  | ||||||
|  |  | ||||||
|   SelectCall &set_option(const std::string &option) { |  | ||||||
|     option_ = option; |  | ||||||
|     return *this; |  | ||||||
|   } |  | ||||||
|   const optional<std::string> &get_option() const { return option_; } |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   Select *const parent_; |  | ||||||
|   optional<std::string> option_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class SelectTraits { |  | ||||||
|  public: |  | ||||||
|   void set_options(std::vector<std::string> options) { this->options_ = std::move(options); } |  | ||||||
|   std::vector<std::string> get_options() const { return this->options_; } |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   std::vector<std::string> options_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** Base-class for all selects. | /** Base-class for all selects. | ||||||
|  * |  * | ||||||
|  * A select can use publish_state to send out a new value. |  * A select can use publish_state to send out a new value. | ||||||
| @@ -51,19 +24,36 @@ class SelectTraits { | |||||||
| class Select : public EntityBase { | class Select : public EntityBase { | ||||||
|  public: |  public: | ||||||
|   std::string state; |   std::string state; | ||||||
|  |   SelectTraits traits; | ||||||
|  |  | ||||||
|   void publish_state(const std::string &state); |   void publish_state(const std::string &state); | ||||||
|  |  | ||||||
|   SelectCall make_call() { return SelectCall(this); } |   /// Return whether this select component has gotten a full state yet. | ||||||
|   void set(const std::string &value) { make_call().set_option(value).perform(); } |  | ||||||
|  |  | ||||||
|   void add_on_state_callback(std::function<void(std::string)> &&callback); |  | ||||||
|  |  | ||||||
|   SelectTraits traits; |  | ||||||
|  |  | ||||||
|   /// Return whether this select has gotten a full state yet. |  | ||||||
|   bool has_state() const { return has_state_; } |   bool has_state() const { return has_state_; } | ||||||
|  |  | ||||||
|  |   /// Instantiate a SelectCall object to modify this select component's state. | ||||||
|  |   SelectCall make_call() { return SelectCall(this); } | ||||||
|  |  | ||||||
|  |   /// Return whether this select component contains the provided option. | ||||||
|  |   bool has_option(const std::string &option) const; | ||||||
|  |  | ||||||
|  |   /// Return whether this select component contains the provided index offset. | ||||||
|  |   bool has_index(size_t index) const; | ||||||
|  |  | ||||||
|  |   /// Return the number of options in this select component. | ||||||
|  |   size_t size() const; | ||||||
|  |  | ||||||
|  |   /// Find the (optional) index offset of the provided option value. | ||||||
|  |   optional<size_t> index_of(const std::string &option) const; | ||||||
|  |  | ||||||
|  |   /// Return the (optional) index offset of the currently active option. | ||||||
|  |   optional<size_t> active_index() const; | ||||||
|  |  | ||||||
|  |   /// Return the (optional) option value at the provided index offset. | ||||||
|  |   optional<std::string> at(size_t index) const; | ||||||
|  |  | ||||||
|  |   void add_on_state_callback(std::function<void(std::string, size_t)> &&callback); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   friend class SelectCall; |   friend class SelectCall; | ||||||
|  |  | ||||||
| @@ -77,7 +67,7 @@ class Select : public EntityBase { | |||||||
|  |  | ||||||
|   uint32_t hash_base() override; |   uint32_t hash_base() override; | ||||||
|  |  | ||||||
|   CallbackManager<void(std::string)> state_callback_; |   CallbackManager<void(std::string, size_t)> state_callback_; | ||||||
|   bool has_state_{false}; |   bool has_state_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										120
									
								
								esphome/components/select/select_call.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								esphome/components/select/select_call.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | #include "select_call.h" | ||||||
|  | #include "select.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace select { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "select"; | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::set_option(const std::string &option) { | ||||||
|  |   return with_operation(SELECT_OP_SET).with_option(option); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::with_operation(SelectOperation operation) { | ||||||
|  |   this->operation_ = operation; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::with_cycle(bool cycle) { | ||||||
|  |   this->cycle_ = cycle; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::with_option(const std::string &option) { | ||||||
|  |   this->option_ = option; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SelectCall &SelectCall::with_index(size_t index) { | ||||||
|  |   this->index_ = index; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SelectCall::perform() { | ||||||
|  |   auto *parent = this->parent_; | ||||||
|  |   const auto *name = parent->get_name().c_str(); | ||||||
|  |   const auto &traits = parent->traits; | ||||||
|  |   auto options = traits.get_options(); | ||||||
|  |  | ||||||
|  |   if (this->operation_ == SELECT_OP_NONE) { | ||||||
|  |     ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (options.empty()) { | ||||||
|  |     ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::string target_value; | ||||||
|  |  | ||||||
|  |   if (this->operation_ == SELECT_OP_SET) { | ||||||
|  |     ESP_LOGD(TAG, "'%s' - Setting", name); | ||||||
|  |     if (!this->option_.has_value()) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     target_value = this->option_.value(); | ||||||
|  |   } else if (this->operation_ == SELECT_OP_SET_INDEX) { | ||||||
|  |     if (!this->index_.has_value()) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (this->index_.value() >= options.size()) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value()); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     target_value = options[this->index_.value()]; | ||||||
|  |   } else if (this->operation_ == SELECT_OP_FIRST) { | ||||||
|  |     target_value = options.front(); | ||||||
|  |   } else if (this->operation_ == SELECT_OP_LAST) { | ||||||
|  |     target_value = options.back(); | ||||||
|  |   } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { | ||||||
|  |     auto cycle = this->cycle_; | ||||||
|  |     ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", | ||||||
|  |              cycle ? "" : "out"); | ||||||
|  |     if (!parent->has_state()) { | ||||||
|  |       target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); | ||||||
|  |     } else { | ||||||
|  |       auto index = parent->index_of(parent->state); | ||||||
|  |       if (index.has_value()) { | ||||||
|  |         auto size = options.size(); | ||||||
|  |         if (cycle) { | ||||||
|  |           auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; | ||||||
|  |           target_value = options[use_index]; | ||||||
|  |         } else { | ||||||
|  |           if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { | ||||||
|  |             target_value = options[index.value() - 1]; | ||||||
|  |           } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { | ||||||
|  |             target_value = options[index.value() + 1]; | ||||||
|  |           } else { | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (std::find(options.begin(), options.end(), target_value) == options.end()) { | ||||||
|  |     ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); | ||||||
|  |   parent->control(target_value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace select | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										47
									
								
								esphome/components/select/select_call.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/select/select_call.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace select { | ||||||
|  |  | ||||||
|  | class Select; | ||||||
|  |  | ||||||
|  | enum SelectOperation { | ||||||
|  |   SELECT_OP_NONE, | ||||||
|  |   SELECT_OP_SET, | ||||||
|  |   SELECT_OP_SET_INDEX, | ||||||
|  |   SELECT_OP_NEXT, | ||||||
|  |   SELECT_OP_PREVIOUS, | ||||||
|  |   SELECT_OP_FIRST, | ||||||
|  |   SELECT_OP_LAST | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SelectCall { | ||||||
|  |  public: | ||||||
|  |   explicit SelectCall(Select *parent) : parent_(parent) {} | ||||||
|  |   void perform(); | ||||||
|  |  | ||||||
|  |   SelectCall &set_option(const std::string &option); | ||||||
|  |   SelectCall &set_index(size_t index); | ||||||
|  |  | ||||||
|  |   SelectCall &select_next(bool cycle); | ||||||
|  |   SelectCall &select_previous(bool cycle); | ||||||
|  |   SelectCall &select_first(); | ||||||
|  |   SelectCall &select_last(); | ||||||
|  |  | ||||||
|  |   SelectCall &with_operation(SelectOperation operation); | ||||||
|  |   SelectCall &with_cycle(bool cycle); | ||||||
|  |   SelectCall &with_option(const std::string &option); | ||||||
|  |   SelectCall &with_index(size_t index); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   Select *const parent_; | ||||||
|  |   optional<std::string> option_; | ||||||
|  |   optional<size_t> index_; | ||||||
|  |   SelectOperation operation_{SELECT_OP_NONE}; | ||||||
|  |   bool cycle_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace select | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										11
									
								
								esphome/components/select/select_traits.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								esphome/components/select/select_traits.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | #include "select_traits.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace select { | ||||||
|  |  | ||||||
|  | void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); } | ||||||
|  |  | ||||||
|  | std::vector<std::string> SelectTraits::get_options() const { return this->options_; } | ||||||
|  |  | ||||||
|  | }  // namespace select | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										19
									
								
								esphome/components/select/select_traits.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/select/select_traits.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  | #include <string> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace select { | ||||||
|  |  | ||||||
|  | class SelectTraits { | ||||||
|  |  public: | ||||||
|  |   void set_options(std::vector<std::string> options); | ||||||
|  |   std::vector<std::string> get_options() const; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::vector<std::string> options_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace select | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										0
									
								
								esphome/components/sen5x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/sen5x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								esphome/components/sen5x/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/sen5x/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "sen5x.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sen5x { | ||||||
|  |  | ||||||
|  | template<typename... Ts> class StartFanAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   SEN5XComponent *sen5x_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sen5x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										413
									
								
								esphome/components/sen5x/sen5x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								esphome/components/sen5x/sen5x.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,413 @@ | |||||||
|  | #include "sen5x.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sen5x { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sen5x"; | ||||||
|  |  | ||||||
|  | static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033; | ||||||
|  | static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1; | ||||||
|  | static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4; | ||||||
|  | static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7; | ||||||
|  | static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607; | ||||||
|  | static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021; | ||||||
|  | static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037; | ||||||
|  | static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86; | ||||||
|  | static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2; | ||||||
|  | static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; | ||||||
|  | static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; | ||||||
|  |  | ||||||
|  | void SEN5XComponent::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Setting up sen5x..."); | ||||||
|  |  | ||||||
|  |   // the sensor needs 1000 ms to enter the idle state | ||||||
|  |   this->set_timeout(1000, [this]() { | ||||||
|  |     // Check if measurement is ready before reading the value | ||||||
|  |     if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to write data ready status command"); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     uint16_t raw_read_status; | ||||||
|  |     if (!this->read_data(raw_read_status)) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to read data ready status"); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     uint32_t stop_measurement_delay = 0; | ||||||
|  |     // In order to query the device periodic measurement must be ceased | ||||||
|  |     if (raw_read_status) { | ||||||
|  |       ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); | ||||||
|  |       if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to stop measurements"); | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after | ||||||
|  |       // issuing the stop_periodic_measurement command | ||||||
|  |       stop_measurement_delay = 200; | ||||||
|  |     } | ||||||
|  |     this->set_timeout(stop_measurement_delay, [this]() { | ||||||
|  |       uint16_t raw_serial_number[3]; | ||||||
|  |       if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to read serial number"); | ||||||
|  |         this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF); | ||||||
|  |       this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF); | ||||||
|  |       this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8); | ||||||
|  |       ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); | ||||||
|  |  | ||||||
|  |       uint16_t raw_product_name[16]; | ||||||
|  |       if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to read product name"); | ||||||
|  |         this->error_code_ = PRODUCT_NAME_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // 2 ASCII bytes are encoded in an int | ||||||
|  |       const uint16_t *current_int = raw_product_name; | ||||||
|  |       char current_char; | ||||||
|  |       uint8_t max = 16; | ||||||
|  |       do { | ||||||
|  |         // first char | ||||||
|  |         current_char = *current_int >> 8; | ||||||
|  |         if (current_char) { | ||||||
|  |           product_name_.push_back(current_char); | ||||||
|  |           // second char | ||||||
|  |           current_char = *current_int & 0xFF; | ||||||
|  |           if (current_char) | ||||||
|  |             product_name_.push_back(current_char); | ||||||
|  |         } | ||||||
|  |         current_int++; | ||||||
|  |       } while (current_char && --max); | ||||||
|  |  | ||||||
|  |       Sen5xType sen5x_type = UNKNOWN; | ||||||
|  |       if (product_name_ == "SEN50") { | ||||||
|  |         sen5x_type = SEN50; | ||||||
|  |       } else { | ||||||
|  |         if (product_name_ == "SEN54") { | ||||||
|  |           sen5x_type = SEN54; | ||||||
|  |         } else { | ||||||
|  |           if (product_name_ == "SEN55") { | ||||||
|  |             sen5x_type = SEN55; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); | ||||||
|  |       } | ||||||
|  |       if (this->humidity_sensor_ && sen5x_type == SEN50) { | ||||||
|  |         ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", | ||||||
|  |                  this->product_name_.c_str()); | ||||||
|  |         this->humidity_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |       if (this->temperature_sensor_ && sen5x_type == SEN50) { | ||||||
|  |         ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", | ||||||
|  |                  this->product_name_.c_str()); | ||||||
|  |         this->temperature_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |       if (this->voc_sensor_ && sen5x_type == SEN50) { | ||||||
|  |         ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); | ||||||
|  |         this->voc_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |       if (this->nox_sensor_ && sen5x_type != SEN55) { | ||||||
|  |         ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); | ||||||
|  |         this->nox_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to read firmware version"); | ||||||
|  |         this->error_code_ = FIRMWARE_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       this->firmware_version_ >>= 8; | ||||||
|  |       ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); | ||||||
|  |  | ||||||
|  |       if (this->voc_sensor_ && this->store_baseline_) { | ||||||
|  |         // Hash with compilation time | ||||||
|  |         // This ensures the baseline storage is cleared after OTA | ||||||
|  |         uint32_t hash = fnv1_hash(App.get_compilation_time()); | ||||||
|  |         this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true); | ||||||
|  |  | ||||||
|  |         if (this->pref_.load(&this->voc_baselines_storage_)) { | ||||||
|  |           ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, | ||||||
|  |                    voc_baselines_storage_.state1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Initialize storage timestamp | ||||||
|  |         this->seconds_since_last_store_ = 0; | ||||||
|  |  | ||||||
|  |         if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { | ||||||
|  |           ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", | ||||||
|  |                    this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); | ||||||
|  |           uint16_t states[4]; | ||||||
|  |  | ||||||
|  |           states[0] = voc_baselines_storage_.state0 >> 16; | ||||||
|  |           states[1] = voc_baselines_storage_.state0 & 0xFFFF; | ||||||
|  |           states[2] = voc_baselines_storage_.state1 >> 16; | ||||||
|  |           states[3] = voc_baselines_storage_.state1 & 0xFFFF; | ||||||
|  |  | ||||||
|  |           if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { | ||||||
|  |             ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       bool result; | ||||||
|  |       if (this->auto_cleaning_interval_.has_value()) { | ||||||
|  |         // override default value | ||||||
|  |         result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); | ||||||
|  |       } else { | ||||||
|  |         result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); | ||||||
|  |       } | ||||||
|  |       if (result) { | ||||||
|  |         delay(20); | ||||||
|  |         uint16_t secs[2]; | ||||||
|  |         if (this->read_data(secs, 2)) { | ||||||
|  |           auto_cleaning_interval_ = secs[0] << 16 | secs[1]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (acceleration_mode_.has_value()) { | ||||||
|  |         result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); | ||||||
|  |       } else { | ||||||
|  |         result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); | ||||||
|  |       } | ||||||
|  |       if (!result) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to set rh/t acceleration mode"); | ||||||
|  |         this->error_code_ = COMMUNICATION_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       delay(20); | ||||||
|  |       if (!acceleration_mode_.has_value()) { | ||||||
|  |         uint16_t mode; | ||||||
|  |         if (this->read_data(mode)) { | ||||||
|  |           this->acceleration_mode_ = RhtAccelerationMode(mode); | ||||||
|  |         } else { | ||||||
|  |           ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (this->voc_tuning_params_.has_value()) | ||||||
|  |         this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); | ||||||
|  |       if (this->nox_tuning_params_.has_value()) | ||||||
|  |         this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); | ||||||
|  |  | ||||||
|  |       if (this->temperature_compensation_.has_value()) | ||||||
|  |         this->write_temperature_compensation_(this->temperature_compensation_.value()); | ||||||
|  |  | ||||||
|  |       // Finally start sensor measurements | ||||||
|  |       auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; | ||||||
|  |       if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) { | ||||||
|  |         // if any of the gas sensors are active we need a full measurement | ||||||
|  |         cmd = SEN5X_CMD_START_MEASUREMENTS; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!this->write_command(cmd)) { | ||||||
|  |         ESP_LOGE(TAG, "Error starting continuous measurements."); | ||||||
|  |         this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       initialized_ = true; | ||||||
|  |       ESP_LOGD(TAG, "Sensor initialized"); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SEN5XComponent::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "sen5x:"); | ||||||
|  |   LOG_I2C_DEVICE(this); | ||||||
|  |   if (this->is_failed()) { | ||||||
|  |     switch (this->error_code_) { | ||||||
|  |       case COMMUNICATION_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); | ||||||
|  |         break; | ||||||
|  |       case MEASUREMENT_INIT_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Measurement Initialization failed!"); | ||||||
|  |         break; | ||||||
|  |       case SERIAL_NUMBER_IDENTIFICATION_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Unable to read sensor serial id"); | ||||||
|  |         break; | ||||||
|  |       case PRODUCT_NAME_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Unable to read product name"); | ||||||
|  |         break; | ||||||
|  |       case FIRMWARE_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Unable to read sensor firmware version"); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         ESP_LOGW(TAG, "Unknown setup error!"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Productname: %s", this->product_name_.c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Firmware version: %d", this->firmware_version_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); | ||||||
|  |   if (this->auto_cleaning_interval_.has_value()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value()); | ||||||
|  |   } | ||||||
|  |   if (this->acceleration_mode_.has_value()) { | ||||||
|  |     switch (this->acceleration_mode_.value()) { | ||||||
|  |       case LOW_ACCELERATION: | ||||||
|  |         ESP_LOGCONFIG(TAG, "  Low RH/T acceleration mode"); | ||||||
|  |         break; | ||||||
|  |       case MEDIUM_ACCELERATION: | ||||||
|  |         ESP_LOGCONFIG(TAG, "  Medium RH/T accelertion mode"); | ||||||
|  |         break; | ||||||
|  |       case HIGH_ACCELERATION: | ||||||
|  |         ESP_LOGCONFIG(TAG, "  High RH/T accelertion mode"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   LOG_UPDATE_INTERVAL(this); | ||||||
|  |   LOG_SENSOR("  ", "PM  1.0", this->pm_1_0_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "PM  2.5", this->pm_2_5_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "PM  4.0", this->pm_4_0_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "PM 10.0", this->pm_10_0_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "VOC", this->voc_sensor_);  // SEN54 and SEN55 only | ||||||
|  |   LOG_SENSOR("  ", "NOx", this->nox_sensor_);  // SEN55 only | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SEN5XComponent::update() { | ||||||
|  |   if (!initialized_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Store baselines after defined interval or if the difference between current and stored baseline becomes too | ||||||
|  |   // much | ||||||
|  |   if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { | ||||||
|  |     if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { | ||||||
|  |       // run it a bit later to avoid adding a delay here | ||||||
|  |       this->set_timeout(550, [this]() { | ||||||
|  |         uint16_t states[4]; | ||||||
|  |         if (this->read_data(states, 4)) { | ||||||
|  |           uint32_t state0 = states[0] << 16 | states[1]; | ||||||
|  |           uint32_t state1 = states[2] << 16 | states[3]; | ||||||
|  |           if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) > | ||||||
|  |                   MAXIMUM_STORAGE_DIFF || | ||||||
|  |               (uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) > | ||||||
|  |                   MAXIMUM_STORAGE_DIFF) { | ||||||
|  |             this->seconds_since_last_store_ = 0; | ||||||
|  |             this->voc_baselines_storage_.state0 = state0; | ||||||
|  |             this->voc_baselines_storage_.state1 = state1; | ||||||
|  |  | ||||||
|  |             if (this->pref_.save(&this->voc_baselines_storage_)) { | ||||||
|  |               ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, | ||||||
|  |                        voc_baselines_storage_.state1); | ||||||
|  |             } else { | ||||||
|  |               ESP_LOGW(TAG, "Could not store VOC baselines"); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->set_timeout(20, [this]() { | ||||||
|  |     uint16_t measurements[8]; | ||||||
|  |  | ||||||
|  |     if (!this->read_data(measurements, 8)) { | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       ESP_LOGD(TAG, "read data error (%d)", this->last_error_); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     float pm_1_0 = measurements[0] / 10.0; | ||||||
|  |     if (measurements[0] == 0xFFFF) | ||||||
|  |       pm_1_0 = NAN; | ||||||
|  |     float pm_2_5 = measurements[1] / 10.0; | ||||||
|  |     if (measurements[1] == 0xFFFF) | ||||||
|  |       pm_2_5 = NAN; | ||||||
|  |     float pm_4_0 = measurements[2] / 10.0; | ||||||
|  |     if (measurements[2] == 0xFFFF) | ||||||
|  |       pm_4_0 = NAN; | ||||||
|  |     float pm_10_0 = measurements[3] / 10.0; | ||||||
|  |     if (measurements[3] == 0xFFFF) | ||||||
|  |       pm_10_0 = NAN; | ||||||
|  |     float humidity = measurements[4] / 100.0; | ||||||
|  |     if (measurements[4] == 0xFFFF) | ||||||
|  |       humidity = NAN; | ||||||
|  |     float temperature = measurements[5] / 200.0; | ||||||
|  |     if (measurements[5] == 0xFFFF) | ||||||
|  |       temperature = NAN; | ||||||
|  |     float voc = measurements[6] / 10.0; | ||||||
|  |     if (measurements[6] == 0xFFFF) | ||||||
|  |       voc = NAN; | ||||||
|  |     float nox = measurements[7] / 10.0; | ||||||
|  |     if (measurements[7] == 0xFFFF) | ||||||
|  |       nox = NAN; | ||||||
|  |  | ||||||
|  |     if (this->pm_1_0_sensor_ != nullptr) | ||||||
|  |       this->pm_1_0_sensor_->publish_state(pm_1_0); | ||||||
|  |     if (this->pm_2_5_sensor_ != nullptr) | ||||||
|  |       this->pm_2_5_sensor_->publish_state(pm_2_5); | ||||||
|  |     if (this->pm_4_0_sensor_ != nullptr) | ||||||
|  |       this->pm_4_0_sensor_->publish_state(pm_4_0); | ||||||
|  |     if (this->pm_10_0_sensor_ != nullptr) | ||||||
|  |       this->pm_10_0_sensor_->publish_state(pm_10_0); | ||||||
|  |     if (this->temperature_sensor_ != nullptr) | ||||||
|  |       this->temperature_sensor_->publish_state(temperature); | ||||||
|  |     if (this->humidity_sensor_ != nullptr) | ||||||
|  |       this->humidity_sensor_->publish_state(humidity); | ||||||
|  |     if (this->voc_sensor_ != nullptr) | ||||||
|  |       this->voc_sensor_->publish_state(voc); | ||||||
|  |     if (this->nox_sensor_ != nullptr) | ||||||
|  |       this->nox_sensor_->publish_state(nox); | ||||||
|  |     this->status_clear_warning(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) { | ||||||
|  |   uint16_t params[6]; | ||||||
|  |   params[0] = tuning.index_offset; | ||||||
|  |   params[1] = tuning.learning_time_offset_hours; | ||||||
|  |   params[2] = tuning.learning_time_gain_hours; | ||||||
|  |   params[3] = tuning.gating_max_duration_minutes; | ||||||
|  |   params[4] = tuning.std_initial; | ||||||
|  |   params[5] = tuning.gain_factor; | ||||||
|  |   auto result = write_command(i2c_command, params, 6); | ||||||
|  |   if (!result) { | ||||||
|  |     ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); | ||||||
|  |   } | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) { | ||||||
|  |   uint16_t params[3]; | ||||||
|  |   params[0] = compensation.offset; | ||||||
|  |   params[1] = compensation.normalized_offset_slope; | ||||||
|  |   params[2] = compensation.time_constant; | ||||||
|  |   if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { | ||||||
|  |     ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SEN5XComponent::start_fan_cleaning() { | ||||||
|  |   if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); | ||||||
|  |     return false; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGD(TAG, "Fan auto clean started"); | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sen5x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										128
									
								
								esphome/components/sen5x/sen5x.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								esphome/components/sen5x/sen5x.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/sensirion_common/i2c_sensirion.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "esphome/core/preferences.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sen5x { | ||||||
|  |  | ||||||
|  | enum ERRORCODE { | ||||||
|  |   COMMUNICATION_FAILED, | ||||||
|  |   SERIAL_NUMBER_IDENTIFICATION_FAILED, | ||||||
|  |   MEASUREMENT_INIT_FAILED, | ||||||
|  |   PRODUCT_NAME_FAILED, | ||||||
|  |   FIRMWARE_FAILED, | ||||||
|  |   UNKNOWN | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Shortest time interval of 3H for storing baseline values. | ||||||
|  | // Prevents wear of the flash because of too many write operations | ||||||
|  | const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; | ||||||
|  | // Store anyway if the baseline difference exceeds the max storage diff value | ||||||
|  | const uint32_t MAXIMUM_STORAGE_DIFF = 50; | ||||||
|  |  | ||||||
|  | struct Sen5xBaselines { | ||||||
|  |   int32_t state0; | ||||||
|  |   int32_t state1; | ||||||
|  | } PACKED;  // NOLINT | ||||||
|  |  | ||||||
|  | enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; | ||||||
|  |  | ||||||
|  | struct GasTuning { | ||||||
|  |   uint16_t index_offset; | ||||||
|  |   uint16_t learning_time_offset_hours; | ||||||
|  |   uint16_t learning_time_gain_hours; | ||||||
|  |   uint16_t gating_max_duration_minutes; | ||||||
|  |   uint16_t std_initial; | ||||||
|  |   uint16_t gain_factor; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct TemperatureCompensation { | ||||||
|  |   uint16_t offset; | ||||||
|  |   uint16_t normalized_offset_slope; | ||||||
|  |   uint16_t time_constant; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { | ||||||
|  |  public: | ||||||
|  |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void update() override; | ||||||
|  |  | ||||||
|  |   enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN }; | ||||||
|  |  | ||||||
|  |   void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } | ||||||
|  |   void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } | ||||||
|  |   void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } | ||||||
|  |   void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } | ||||||
|  |  | ||||||
|  |   void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } | ||||||
|  |   void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } | ||||||
|  |   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } | ||||||
|  |   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } | ||||||
|  |   void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } | ||||||
|  |   void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; } | ||||||
|  |   void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; } | ||||||
|  |   void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, | ||||||
|  |                                 uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, | ||||||
|  |                                 uint16_t std_initial, uint16_t gain_factor) { | ||||||
|  |     voc_tuning_params_.value().index_offset = index_offset; | ||||||
|  |     voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; | ||||||
|  |     voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; | ||||||
|  |     voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; | ||||||
|  |     voc_tuning_params_.value().std_initial = std_initial; | ||||||
|  |     voc_tuning_params_.value().gain_factor = gain_factor; | ||||||
|  |   } | ||||||
|  |   void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, | ||||||
|  |                                 uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, | ||||||
|  |                                 uint16_t gain_factor) { | ||||||
|  |     nox_tuning_params_.value().index_offset = index_offset; | ||||||
|  |     nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; | ||||||
|  |     nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; | ||||||
|  |     nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; | ||||||
|  |     nox_tuning_params_.value().std_initial = 50; | ||||||
|  |     nox_tuning_params_.value().gain_factor = gain_factor; | ||||||
|  |   } | ||||||
|  |   void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { | ||||||
|  |     temperature_compensation_.value().offset = offset * 200; | ||||||
|  |     temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; | ||||||
|  |     temperature_compensation_.value().time_constant = time_constant; | ||||||
|  |   } | ||||||
|  |   bool start_fan_cleaning(); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); | ||||||
|  |   bool write_temperature_compensation_(const TemperatureCompensation &compensation); | ||||||
|  |   ERRORCODE error_code_; | ||||||
|  |   bool initialized_{false}; | ||||||
|  |   sensor::Sensor *pm_1_0_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pm_2_5_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pm_4_0_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pm_10_0_sensor_{nullptr}; | ||||||
|  |   // SEN54 and SEN55 only | ||||||
|  |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *voc_sensor_{nullptr}; | ||||||
|  |   // SEN55 only | ||||||
|  |   sensor::Sensor *nox_sensor_{nullptr}; | ||||||
|  |  | ||||||
|  |   std::string product_name_; | ||||||
|  |   uint8_t serial_number_[4]; | ||||||
|  |   uint16_t firmware_version_; | ||||||
|  |   Sen5xBaselines voc_baselines_storage_; | ||||||
|  |   bool store_baseline_; | ||||||
|  |   uint32_t seconds_since_last_store_; | ||||||
|  |   ESPPreferenceObject pref_; | ||||||
|  |   optional<RhtAccelerationMode> acceleration_mode_; | ||||||
|  |   optional<uint32_t> auto_cleaning_interval_; | ||||||
|  |   optional<GasTuning> voc_tuning_params_; | ||||||
|  |   optional<GasTuning> nox_tuning_params_; | ||||||
|  |   optional<TemperatureCompensation> temperature_compensation_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sen5x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										241
									
								
								esphome/components/sen5x/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								esphome/components/sen5x/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import i2c, sensor, sensirion_common | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.automation import maybe_simple_id | ||||||
|  |  | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_HUMIDITY, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_OFFSET, | ||||||
|  |     CONF_PM_1_0, | ||||||
|  |     CONF_PM_10_0, | ||||||
|  |     CONF_PM_2_5, | ||||||
|  |     CONF_PM_4_0, | ||||||
|  |     CONF_STORE_BASELINE, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_HUMIDITY, | ||||||
|  |     DEVICE_CLASS_NITROUS_OXIDE, | ||||||
|  |     DEVICE_CLASS_PM1, | ||||||
|  |     DEVICE_CLASS_PM10, | ||||||
|  |     DEVICE_CLASS_PM25, | ||||||
|  |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|  |     ICON_CHEMICAL_WEAPON, | ||||||
|  |     ICON_RADIATOR, | ||||||
|  |     ICON_THERMOMETER, | ||||||
|  |     ICON_WATER_PERCENT, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_CELSIUS, | ||||||
|  |     UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@martgras"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  | AUTO_LOAD = ["sensirion_common"] | ||||||
|  |  | ||||||
|  | sen5x_ns = cg.esphome_ns.namespace("sen5x") | ||||||
|  | SEN5XComponent = sen5x_ns.class_( | ||||||
|  |     "SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice | ||||||
|  | ) | ||||||
|  | RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode") | ||||||
|  |  | ||||||
|  | CONF_ACCELERATION_MODE = "acceleration_mode" | ||||||
|  | CONF_ALGORITHM_TUNING = "algorithm_tuning" | ||||||
|  | CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" | ||||||
|  | CONF_GAIN_FACTOR = "gain_factor" | ||||||
|  | CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" | ||||||
|  | CONF_INDEX_OFFSET = "index_offset" | ||||||
|  | CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" | ||||||
|  | CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" | ||||||
|  | CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" | ||||||
|  | CONF_NOX = "nox" | ||||||
|  | CONF_STD_INITIAL = "std_initial" | ||||||
|  | CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" | ||||||
|  | CONF_TIME_CONSTANT = "time_constant" | ||||||
|  | CONF_VOC = "voc" | ||||||
|  | CONF_VOC_BASELINE = "voc_baseline" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Actions | ||||||
|  | StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action) | ||||||
|  |  | ||||||
|  | ACCELERATION_MODES = { | ||||||
|  |     "low": RhtAccelerationMode.LOW_ACCELERATION, | ||||||
|  |     "medium": RhtAccelerationMode.MEDIUM_ACCELERATION, | ||||||
|  |     "high": RhtAccelerationMode.HIGH_ACCELERATION, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GAS_SENSOR = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), | ||||||
|  |                 cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( | ||||||
|  |                     1, 1000 | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( | ||||||
|  |                     1, 1000 | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional( | ||||||
|  |                     CONF_GATING_MAX_DURATION_MINUTES, default=720 | ||||||
|  |                 ): cv.int_range(0, 3000), | ||||||
|  |                 cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, | ||||||
|  |                 cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(SEN5XComponent), | ||||||
|  |             cv.Optional(CONF_PM_1_0): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_PM1, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_PM_2_5): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_PM25, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_PM_4_0): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_PM_10_0): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_PM10, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_, | ||||||
|  |             cv.Optional(CONF_VOC): sensor.sensor_schema( | ||||||
|  |                 icon=ICON_RADIATOR, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ).extend(GAS_SENSOR), | ||||||
|  |             cv.Optional(CONF_NOX): sensor.sensor_schema( | ||||||
|  |                 icon=ICON_RADIATOR, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_NITROUS_OXIDE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ).extend(GAS_SENSOR), | ||||||
|  |             cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, | ||||||
|  |             cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, | ||||||
|  |             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |                 icon=ICON_THERMOMETER, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PERCENT, | ||||||
|  |                 icon=ICON_WATER_PERCENT, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( | ||||||
|  |                 { | ||||||
|  |                     cv.Optional(CONF_OFFSET, default=0): cv.float_, | ||||||
|  |                     cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, | ||||||
|  |                     cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("60s")) | ||||||
|  |     .extend(i2c.i2c_device_schema(0x69)) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | SENSOR_MAP = { | ||||||
|  |     CONF_PM_1_0: "set_pm_1_0_sensor", | ||||||
|  |     CONF_PM_2_5: "set_pm_2_5_sensor", | ||||||
|  |     CONF_PM_4_0: "set_pm_4_0_sensor", | ||||||
|  |     CONF_PM_10_0: "set_pm_10_0_sensor", | ||||||
|  |     CONF_VOC: "set_voc_sensor", | ||||||
|  |     CONF_NOX: "set_nox_sensor", | ||||||
|  |     CONF_TEMPERATURE: "set_temperature_sensor", | ||||||
|  |     CONF_HUMIDITY: "set_humidity_sensor", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SETTING_MAP = { | ||||||
|  |     CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", | ||||||
|  |     CONF_ACCELERATION_MODE: "set_acceleration_mode", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
|  |  | ||||||
|  |     for key, funcName in SETTING_MAP.items(): | ||||||
|  |         if key in config: | ||||||
|  |             cg.add(getattr(var, funcName)(config[key])) | ||||||
|  |  | ||||||
|  |     for key, funcName in SENSOR_MAP.items(): | ||||||
|  |         if key in config: | ||||||
|  |             sens = await sensor.new_sensor(config[key]) | ||||||
|  |             cg.add(getattr(var, funcName)(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]: | ||||||
|  |         cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] | ||||||
|  |         cg.add( | ||||||
|  |             var.set_voc_algorithm_tuning( | ||||||
|  |                 cfg[CONF_INDEX_OFFSET], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_OFFSET_HOURS], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_GAIN_HOURS], | ||||||
|  |                 cfg[CONF_GATING_MAX_DURATION_MINUTES], | ||||||
|  |                 cfg[CONF_STD_INITIAL], | ||||||
|  |                 cfg[CONF_GAIN_FACTOR], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]: | ||||||
|  |         cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] | ||||||
|  |         cg.add( | ||||||
|  |             var.set_nox_algorithm_tuning( | ||||||
|  |                 cfg[CONF_INDEX_OFFSET], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_OFFSET_HOURS], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_GAIN_HOURS], | ||||||
|  |                 cfg[CONF_GATING_MAX_DURATION_MINUTES], | ||||||
|  |                 cfg[CONF_GAIN_FACTOR], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     if CONF_TEMPERATURE_COMPENSATION in config: | ||||||
|  |         cg.add( | ||||||
|  |             var.set_temperature_compensation( | ||||||
|  |                 config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], | ||||||
|  |                 config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], | ||||||
|  |                 config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SEN5X_ACTION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(SEN5XComponent), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def sen54_fan_to_code(config, action_id, template_arg, args): | ||||||
|  |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|  |     return cg.new_Pvariable(action_id, template_arg, paren) | ||||||
| @@ -29,6 +29,7 @@ from esphome.const import ( | |||||||
|     CONF_WINDOW_SIZE, |     CONF_WINDOW_SIZE, | ||||||
|     CONF_MQTT_ID, |     CONF_MQTT_ID, | ||||||
|     CONF_FORCE_UPDATE, |     CONF_FORCE_UPDATE, | ||||||
|  |     DEVICE_CLASS_DURATION, | ||||||
|     DEVICE_CLASS_EMPTY, |     DEVICE_CLASS_EMPTY, | ||||||
|     DEVICE_CLASS_AQI, |     DEVICE_CLASS_AQI, | ||||||
|     DEVICE_CLASS_BATTERY, |     DEVICE_CLASS_BATTERY, | ||||||
| @@ -70,6 +71,7 @@ DEVICE_CLASSES = [ | |||||||
|     DEVICE_CLASS_CARBON_DIOXIDE, |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|     DEVICE_CLASS_CARBON_MONOXIDE, |     DEVICE_CLASS_CARBON_MONOXIDE, | ||||||
|     DEVICE_CLASS_CURRENT, |     DEVICE_CLASS_CURRENT, | ||||||
|  |     DEVICE_CLASS_DURATION, | ||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
|     DEVICE_CLASS_GAS, |     DEVICE_CLASS_GAS, | ||||||
|     DEVICE_CLASS_HUMIDITY, |     DEVICE_CLASS_HUMIDITY, | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								esphome/components/sml/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/sml/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | import re | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import uart | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@alengwenus"] | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["uart"] | ||||||
|  |  | ||||||
|  | sml_ns = cg.esphome_ns.namespace("sml") | ||||||
|  | Sml = sml_ns.class_("Sml", cg.Component, uart.UARTDevice) | ||||||
|  | MULTI_CONF = True | ||||||
|  |  | ||||||
|  | CONF_SML_ID = "sml_id" | ||||||
|  | CONF_OBIS_CODE = "obis_code" | ||||||
|  | CONF_SERVER_ID = "server_id" | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(Sml), | ||||||
|  |     } | ||||||
|  | ).extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def obis_code(value): | ||||||
|  |     value = cv.string(value) | ||||||
|  |     match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value) | ||||||
|  |     if match is None: | ||||||
|  |         raise cv.Invalid(f"{value} is not a valid OBIS code") | ||||||
|  |     return value | ||||||
							
								
								
									
										48
									
								
								esphome/components/sml/constants.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								esphome/components/sml/constants.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | enum SmlType : uint8_t { | ||||||
|  |   SML_OCTET = 0, | ||||||
|  |   SML_BOOL = 4, | ||||||
|  |   SML_INT = 5, | ||||||
|  |   SML_UINT = 6, | ||||||
|  |   SML_LIST = 7, | ||||||
|  |   SML_HEX = 10, | ||||||
|  |   SML_UNDEFINED = 255 | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 }; | ||||||
|  |  | ||||||
|  | enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS }; | ||||||
|  |  | ||||||
|  | // masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11 | ||||||
|  | const uint16_t START_MASK = 0x55aa;  // 0x1b 1b 1b 1b 1b 01 01 01 01 | ||||||
|  | const uint16_t END_MASK = 0x0157;    // 0x1b 1b 1b 1b 1a | ||||||
|  |  | ||||||
|  | const uint16_t CRC16_X25_TABLE[256] = { | ||||||
|  |     0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, | ||||||
|  |     0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, | ||||||
|  |     0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, | ||||||
|  |     0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, | ||||||
|  |     0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, | ||||||
|  |     0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, | ||||||
|  |     0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, | ||||||
|  |     0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, | ||||||
|  |     0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, | ||||||
|  |     0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, | ||||||
|  |     0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, | ||||||
|  |     0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, | ||||||
|  |     0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, | ||||||
|  |     0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, | ||||||
|  |     0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, | ||||||
|  |     0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, | ||||||
|  |     0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, | ||||||
|  |     0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, | ||||||
|  |     0x3de3, 0x2c6a, 0x1ef1, 0x0f78}; | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										30
									
								
								esphome/components/sml/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/sml/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import sensor | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["sml"] | ||||||
|  |  | ||||||
|  | SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = sensor.sensor_schema().extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SmlSensor), | ||||||
|  |         cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), | ||||||
|  |         cv.Required(CONF_OBIS_CODE): obis_code, | ||||||
|  |         cv.Optional(CONF_SERVER_ID, default=""): cv.string, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable( | ||||||
|  |         config[CONF_ID], config[CONF_SERVER_ID], config[CONF_OBIS_CODE] | ||||||
|  |     ) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await sensor.register_sensor(var, config) | ||||||
|  |     sml = await cg.get_variable(config[CONF_SML_ID]) | ||||||
|  |     cg.add(sml.register_sml_listener(var)) | ||||||
							
								
								
									
										41
									
								
								esphome/components/sml/sensor/sml_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/sml/sensor/sml_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "sml_sensor.h" | ||||||
|  | #include "../sml_parser.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sml_sensor"; | ||||||
|  |  | ||||||
|  | SmlSensor::SmlSensor(std::string server_id, std::string obis_code) | ||||||
|  |     : SmlListener(std::move(server_id), std::move(obis_code)) {} | ||||||
|  |  | ||||||
|  | void SmlSensor::publish_val(const ObisInfo &obis_info) { | ||||||
|  |   switch (obis_info.value_type) { | ||||||
|  |     case SML_INT: { | ||||||
|  |       publish_state(bytes_to_int(obis_info.value)); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case SML_BOOL: | ||||||
|  |     case SML_UINT: { | ||||||
|  |       publish_state(bytes_to_uint(obis_info.value)); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case SML_OCTET: { | ||||||
|  |       ESP_LOGW(TAG, "No number conversion for (%s) %s. Consider using SML TextSensor instead.", | ||||||
|  |                bytes_repr(obis_info.server_id).c_str(), obis_info.code_repr().c_str()); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SmlSensor::dump_config() { | ||||||
|  |   LOG_SENSOR("", "SML", this); | ||||||
|  |   if (!this->server_id.empty()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Server ID: %s", this->server_id.c_str()); | ||||||
|  |   } | ||||||
|  |   ESP_LOGCONFIG(TAG, "  OBIS Code: %s", this->obis_code.c_str()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										16
									
								
								esphome/components/sml/sensor/sml_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								esphome/components/sml/sensor/sml_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "esphome/components/sml/sml.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | class SmlSensor : public SmlListener, public sensor::Sensor, public Component { | ||||||
|  |  public: | ||||||
|  |   SmlSensor(std::string server_id, std::string obis_code); | ||||||
|  |   void publish_val(const ObisInfo &obis_info) override; | ||||||
|  |   void dump_config() override; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										146
									
								
								esphome/components/sml/sml.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								esphome/components/sml/sml.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | #include "sml.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "sml_parser.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sml"; | ||||||
|  |  | ||||||
|  | const char START_BYTES_DETECTED = 1; | ||||||
|  | const char END_BYTES_DETECTED = 2; | ||||||
|  |  | ||||||
|  | SmlListener::SmlListener(std::string server_id, std::string obis_code) | ||||||
|  |     : server_id(std::move(server_id)), obis_code(std::move(obis_code)) {} | ||||||
|  |  | ||||||
|  | char Sml::check_start_end_bytes_(uint8_t byte) { | ||||||
|  |   this->incoming_mask_ = (this->incoming_mask_ << 2) | get_code(byte); | ||||||
|  |  | ||||||
|  |   if (this->incoming_mask_ == START_MASK) | ||||||
|  |     return START_BYTES_DETECTED; | ||||||
|  |   if ((this->incoming_mask_ >> 6) == END_MASK) | ||||||
|  |     return END_BYTES_DETECTED; | ||||||
|  |   return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Sml::loop() { | ||||||
|  |   while (available()) { | ||||||
|  |     const char c = read(); | ||||||
|  |  | ||||||
|  |     if (this->record_) | ||||||
|  |       this->sml_data_.emplace_back(c); | ||||||
|  |  | ||||||
|  |     switch (this->check_start_end_bytes_(c)) { | ||||||
|  |       case START_BYTES_DETECTED: { | ||||||
|  |         this->record_ = true; | ||||||
|  |         this->sml_data_.clear(); | ||||||
|  |         break; | ||||||
|  |       }; | ||||||
|  |       case END_BYTES_DETECTED: { | ||||||
|  |         if (this->record_) { | ||||||
|  |           this->record_ = false; | ||||||
|  |  | ||||||
|  |           if (!check_sml_data(this->sml_data_)) | ||||||
|  |             break; | ||||||
|  |  | ||||||
|  |           // remove footer bytes | ||||||
|  |           this->sml_data_.resize(this->sml_data_.size() - 8); | ||||||
|  |           this->process_sml_file_(this->sml_data_); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Sml::process_sml_file_(const bytes &sml_data) { | ||||||
|  |   SmlFile sml_file = SmlFile(sml_data); | ||||||
|  |   std::vector<ObisInfo> obis_info = sml_file.get_obis_info(); | ||||||
|  |   this->publish_obis_info_(obis_info); | ||||||
|  |  | ||||||
|  |   this->log_obis_info_(obis_info); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Sml::log_obis_info_(const std::vector<ObisInfo> &obis_info_vec) { | ||||||
|  |   ESP_LOGD(TAG, "OBIS info:"); | ||||||
|  |   for (auto const &obis_info : obis_info_vec) { | ||||||
|  |     std::string info; | ||||||
|  |     info += "  (" + bytes_repr(obis_info.server_id) + ") "; | ||||||
|  |     info += obis_info.code_repr(); | ||||||
|  |     info += " [0x" + bytes_repr(obis_info.value) + "]"; | ||||||
|  |     ESP_LOGD(TAG, "%s", info.c_str()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Sml::publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec) { | ||||||
|  |   for (auto const &obis_info : obis_info_vec) { | ||||||
|  |     this->publish_value_(obis_info); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Sml::publish_value_(const ObisInfo &obis_info) { | ||||||
|  |   for (auto const &sml_listener : sml_listeners_) { | ||||||
|  |     if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id)) | ||||||
|  |       continue; | ||||||
|  |     if (obis_info.code_repr() != sml_listener->obis_code) | ||||||
|  |       continue; | ||||||
|  |     sml_listener->publish_val(obis_info); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Sml::dump_config() { ESP_LOGCONFIG(TAG, "SML:"); } | ||||||
|  |  | ||||||
|  | void Sml::register_sml_listener(SmlListener *listener) { sml_listeners_.emplace_back(listener); } | ||||||
|  |  | ||||||
|  | bool check_sml_data(const bytes &buffer) { | ||||||
|  |   if (buffer.size() < 2) { | ||||||
|  |     ESP_LOGW(TAG, "Checksum error in received SML data."); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1); | ||||||
|  |   if (crc_received == calc_crc16_x25(buffer.begin(), buffer.end() - 2, 0x6e23)) { | ||||||
|  |     ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25."); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (crc_received == calc_crc16_kermit(buffer.begin(), buffer.end() - 2, 0xed50)) { | ||||||
|  |     ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT."); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGW(TAG, "Checksum error in received SML data."); | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum) { | ||||||
|  |   for (auto it = begin; it != end; it++) { | ||||||
|  |     crcsum = (crcsum >> 8) ^ CRC16_X25_TABLE[(crcsum & 0xff) ^ *it]; | ||||||
|  |   } | ||||||
|  |   return crcsum; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { | ||||||
|  |   crcsum = calc_crc16_p1021(begin, end, crcsum ^ 0xffff) ^ 0xffff; | ||||||
|  |   return (crcsum >> 8) | ((crcsum & 0xff) << 8); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { | ||||||
|  |   return calc_crc16_p1021(begin, end, crcsum); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t get_code(uint8_t byte) { | ||||||
|  |   switch (byte) { | ||||||
|  |     case 0x1b: | ||||||
|  |       return 1; | ||||||
|  |     case 0x01: | ||||||
|  |       return 2; | ||||||
|  |     case 0x1a: | ||||||
|  |       return 3; | ||||||
|  |     default: | ||||||
|  |       return 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										47
									
								
								esphome/components/sml/sml.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/sml/sml.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <string> | ||||||
|  | #include <vector> | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  | #include "sml_parser.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | class SmlListener { | ||||||
|  |  public: | ||||||
|  |   std::string server_id; | ||||||
|  |   std::string obis_code; | ||||||
|  |   SmlListener(std::string server_id, std::string obis_code); | ||||||
|  |   virtual void publish_val(const ObisInfo &obis_info){}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Sml : public Component, public uart::UARTDevice { | ||||||
|  |  public: | ||||||
|  |   void register_sml_listener(SmlListener *listener); | ||||||
|  |   void loop() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   std::vector<SmlListener *> sml_listeners_{}; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void process_sml_file_(const bytes &sml_data); | ||||||
|  |   void log_obis_info_(const std::vector<ObisInfo> &obis_info_vec); | ||||||
|  |   void publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec); | ||||||
|  |   char check_start_end_bytes_(uint8_t byte); | ||||||
|  |   void publish_value_(const ObisInfo &obis_info); | ||||||
|  |  | ||||||
|  |   // Serial parser | ||||||
|  |   bool record_ = false; | ||||||
|  |   uint16_t incoming_mask_ = 0; | ||||||
|  |   bytes sml_data_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | bool check_sml_data(const bytes &buffer); | ||||||
|  | uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); | ||||||
|  | uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); | ||||||
|  | uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); | ||||||
|  |  | ||||||
|  | uint8_t get_code(uint8_t byte); | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										131
									
								
								esphome/components/sml/sml_parser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								esphome/components/sml/sml_parser.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "constants.h" | ||||||
|  | #include "sml_parser.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { | ||||||
|  |   // extract messages | ||||||
|  |   this->pos_ = 0; | ||||||
|  |   while (this->pos_ < this->buffer_.size()) { | ||||||
|  |     if (this->buffer_[this->pos_] == 0x00) | ||||||
|  |       break;  // fill byte detected -> no more messages | ||||||
|  |  | ||||||
|  |     SmlNode message = SmlNode(); | ||||||
|  |     if (!this->setup_node(&message)) | ||||||
|  |       break; | ||||||
|  |     this->messages.emplace_back(message); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SmlFile::setup_node(SmlNode *node) { | ||||||
|  |   uint8_t type = this->buffer_[this->pos_] >> 4;      // type including overlength info | ||||||
|  |   uint8_t length = this->buffer_[this->pos_] & 0x0f;  // length including TL bytes | ||||||
|  |   bool is_list = (type & 0x07) == SML_LIST; | ||||||
|  |   bool has_extended_length = type & 0x08;  // we have a long list/value (>15 entries) | ||||||
|  |   uint8_t parse_length = length; | ||||||
|  |   if (has_extended_length) { | ||||||
|  |     length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); | ||||||
|  |     parse_length = length - 1; | ||||||
|  |     this->pos_ += 1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->pos_ + parse_length >= this->buffer_.size()) | ||||||
|  |     return false; | ||||||
|  |  | ||||||
|  |   node->type = type & 0x07; | ||||||
|  |   node->nodes.clear(); | ||||||
|  |   node->value_bytes.clear(); | ||||||
|  |   if (this->buffer_[this->pos_] == 0x00) {  // end of message | ||||||
|  |     this->pos_ += 1; | ||||||
|  |   } else if (is_list) {  // list | ||||||
|  |     this->pos_ += 1; | ||||||
|  |     node->nodes.reserve(parse_length); | ||||||
|  |     for (size_t i = 0; i != parse_length; i++) { | ||||||
|  |       SmlNode child_node = SmlNode(); | ||||||
|  |       if (!this->setup_node(&child_node)) | ||||||
|  |         return false; | ||||||
|  |       node->nodes.emplace_back(child_node); | ||||||
|  |     } | ||||||
|  |   } else {  // value | ||||||
|  |     node->value_bytes = | ||||||
|  |         bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length); | ||||||
|  |     this->pos_ += parse_length; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::vector<ObisInfo> SmlFile::get_obis_info() { | ||||||
|  |   std::vector<ObisInfo> obis_info; | ||||||
|  |   for (auto const &message : messages) { | ||||||
|  |     SmlNode message_body = message.nodes[3]; | ||||||
|  |     uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes); | ||||||
|  |     if (message_type != SML_GET_LIST_RES) | ||||||
|  |       continue; | ||||||
|  |  | ||||||
|  |     SmlNode get_list_response = message_body.nodes[1]; | ||||||
|  |     bytes server_id = get_list_response.nodes[1].value_bytes; | ||||||
|  |     SmlNode val_list = get_list_response.nodes[4]; | ||||||
|  |  | ||||||
|  |     for (auto const &val_list_entry : val_list.nodes) { | ||||||
|  |       obis_info.emplace_back(server_id, val_list_entry); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return obis_info; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string bytes_repr(const bytes &buffer) { | ||||||
|  |   std::string repr; | ||||||
|  |   for (auto const value : buffer) { | ||||||
|  |     repr += str_sprintf("%02x", value & 0xff); | ||||||
|  |   } | ||||||
|  |   return repr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint64_t bytes_to_uint(const bytes &buffer) { | ||||||
|  |   uint64_t val = 0; | ||||||
|  |   for (auto const value : buffer) { | ||||||
|  |     val = (val << 8) + value; | ||||||
|  |   } | ||||||
|  |   return val; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | int64_t bytes_to_int(const bytes &buffer) { | ||||||
|  |   uint64_t tmp = bytes_to_uint(buffer); | ||||||
|  |   int64_t val; | ||||||
|  |  | ||||||
|  |   switch (buffer.size()) { | ||||||
|  |     case 1:  // int8 | ||||||
|  |       val = (int8_t) tmp; | ||||||
|  |       break; | ||||||
|  |     case 2:  // int16 | ||||||
|  |       val = (int16_t) tmp; | ||||||
|  |       break; | ||||||
|  |     case 4:  // int32 | ||||||
|  |       val = (int32_t) tmp; | ||||||
|  |       break; | ||||||
|  |     default:  // int64 | ||||||
|  |       val = (int64_t) tmp; | ||||||
|  |   } | ||||||
|  |   return val; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); } | ||||||
|  |  | ||||||
|  | ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) { | ||||||
|  |   this->code = val_list_entry.nodes[0].value_bytes; | ||||||
|  |   this->status = val_list_entry.nodes[1].value_bytes; | ||||||
|  |   this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes); | ||||||
|  |   this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes); | ||||||
|  |   SmlNode value_node = val_list_entry.nodes[5]; | ||||||
|  |   this->value = value_node.value_bytes; | ||||||
|  |   this->value_type = value_node.type; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string ObisInfo::code_repr() const { | ||||||
|  |   return str_sprintf("%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], this->code[4]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										54
									
								
								esphome/components/sml/sml_parser.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								esphome/components/sml/sml_parser.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  | #include <cstdio> | ||||||
|  | #include <string> | ||||||
|  | #include <vector> | ||||||
|  | #include "constants.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | using bytes = std::vector<uint8_t>; | ||||||
|  |  | ||||||
|  | class SmlNode { | ||||||
|  |  public: | ||||||
|  |   uint8_t type; | ||||||
|  |   bytes value_bytes; | ||||||
|  |   std::vector<SmlNode> nodes; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class ObisInfo { | ||||||
|  |  public: | ||||||
|  |   ObisInfo(bytes server_id, SmlNode val_list_entry); | ||||||
|  |   bytes server_id; | ||||||
|  |   bytes code; | ||||||
|  |   bytes status; | ||||||
|  |   char unit; | ||||||
|  |   char scaler; | ||||||
|  |   bytes value; | ||||||
|  |   uint16_t value_type; | ||||||
|  |   std::string code_repr() const; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SmlFile { | ||||||
|  |  public: | ||||||
|  |   SmlFile(bytes buffer); | ||||||
|  |   bool setup_node(SmlNode *node); | ||||||
|  |   std::vector<SmlNode> messages; | ||||||
|  |   std::vector<ObisInfo> get_obis_info(); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   const bytes buffer_; | ||||||
|  |   size_t pos_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | std::string bytes_repr(const bytes &buffer); | ||||||
|  |  | ||||||
|  | uint64_t bytes_to_uint(const bytes &buffer); | ||||||
|  |  | ||||||
|  | int64_t bytes_to_int(const bytes &buffer); | ||||||
|  |  | ||||||
|  | std::string bytes_to_string(const bytes &buffer); | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										43
									
								
								esphome/components/sml/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/sml/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import text_sensor | ||||||
|  | from esphome.const import CONF_FORMAT, CONF_ID | ||||||
|  |  | ||||||
|  | from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["sml"] | ||||||
|  |  | ||||||
|  | SmlType = sml_ns.enum("SmlType") | ||||||
|  | SML_TYPES = { | ||||||
|  |     "text": SmlType.SML_OCTET, | ||||||
|  |     "bool": SmlType.SML_BOOL, | ||||||
|  |     "int": SmlType.SML_INT, | ||||||
|  |     "uint": SmlType.SML_UINT, | ||||||
|  |     "hex": SmlType.SML_HEX, | ||||||
|  |     "": SmlType.SML_UNDEFINED, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SmlTextSensor), | ||||||
|  |         cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), | ||||||
|  |         cv.Required(CONF_OBIS_CODE): obis_code, | ||||||
|  |         cv.Optional(CONF_SERVER_ID, default=""): cv.string, | ||||||
|  |         cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable( | ||||||
|  |         config[CONF_ID], | ||||||
|  |         config[CONF_SERVER_ID], | ||||||
|  |         config[CONF_OBIS_CODE], | ||||||
|  |         config[CONF_FORMAT], | ||||||
|  |     ) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await text_sensor.register_text_sensor(var, config) | ||||||
|  |     sml = await cg.get_variable(config[CONF_SML_ID]) | ||||||
|  |     cg.add(sml.register_sml_listener(var)) | ||||||
							
								
								
									
										54
									
								
								esphome/components/sml/text_sensor/sml_text_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								esphome/components/sml/text_sensor/sml_text_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "sml_text_sensor.h" | ||||||
|  | #include "../sml_parser.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sml_text_sensor"; | ||||||
|  |  | ||||||
|  | SmlTextSensor::SmlTextSensor(std::string server_id, std::string obis_code, SmlType format) | ||||||
|  |     : SmlListener(std::move(server_id), std::move(obis_code)), format_(format) {} | ||||||
|  |  | ||||||
|  | void SmlTextSensor::publish_val(const ObisInfo &obis_info) { | ||||||
|  |   uint8_t value_type; | ||||||
|  |   if (this->format_ == SML_UNDEFINED) { | ||||||
|  |     value_type = obis_info.value_type; | ||||||
|  |   } else { | ||||||
|  |     value_type = this->format_; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   switch (value_type) { | ||||||
|  |     case SML_HEX: { | ||||||
|  |       publish_state("0x" + bytes_repr(obis_info.value)); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case SML_INT: { | ||||||
|  |       publish_state(to_string(bytes_to_int(obis_info.value))); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case SML_BOOL: | ||||||
|  |       publish_state(bytes_to_uint(obis_info.value) ? "True" : "False"); | ||||||
|  |       break; | ||||||
|  |     case SML_UINT: { | ||||||
|  |       publish_state(to_string(bytes_to_uint(obis_info.value))); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case SML_OCTET: { | ||||||
|  |       publish_state(std::string(obis_info.value.begin(), obis_info.value.end())); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SmlTextSensor::dump_config() { | ||||||
|  |   LOG_TEXT_SENSOR("", "SML", this); | ||||||
|  |   if (!this->server_id.empty()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Server ID: %s", this->server_id.c_str()); | ||||||
|  |   } | ||||||
|  |   ESP_LOGCONFIG(TAG, "  OBIS Code: %s", this->obis_code.c_str()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										21
									
								
								esphome/components/sml/text_sensor/sml_text_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/sml/text_sensor/sml_text_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/sml/sml.h" | ||||||
|  | #include "esphome/components/text_sensor/text_sensor.h" | ||||||
|  | #include "../constants.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sml { | ||||||
|  |  | ||||||
|  | class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public Component { | ||||||
|  |  public: | ||||||
|  |   SmlTextSensor(std::string server_id, std::string obis_code, SmlType format); | ||||||
|  |   void publish_val(const ObisInfo &obis_info) override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   SmlType format_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sml | ||||||
|  | }  // namespace esphome | ||||||
| @@ -156,15 +156,17 @@ class SPIComponent : public Component { | |||||||
|  |  | ||||||
|   template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> |   template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> | ||||||
|   uint8_t transfer_byte(uint8_t data) { |   uint8_t transfer_byte(uint8_t data) { | ||||||
| #ifdef USE_SPI_ARDUINO_BACKEND |  | ||||||
|     if (this->miso_ != nullptr) { |     if (this->miso_ != nullptr) { | ||||||
|  | #ifdef USE_SPI_ARDUINO_BACKEND | ||||||
|       if (this->hw_spi_ != nullptr) { |       if (this->hw_spi_ != nullptr) { | ||||||
|         return this->hw_spi_->transfer(data); |         return this->hw_spi_->transfer(data); | ||||||
|       } else { |       } else { | ||||||
|  | #endif  // USE_SPI_ARDUINO_BACKEND | ||||||
|         return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data); |         return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data); | ||||||
|       } | #ifdef USE_SPI_ARDUINO_BACKEND | ||||||
|       } |       } | ||||||
| #endif  // USE_SPI_ARDUINO_BACKEND | #endif  // USE_SPI_ARDUINO_BACKEND | ||||||
|  |     } | ||||||
|     this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data); |     this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data); | ||||||
|     return 0; |     return 0; | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								esphome/components/sps30/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/sps30/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "sps30.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sps30 { | ||||||
|  |  | ||||||
|  | template<typename... Ts> class StartFanAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->sps30_->start_fan_cleaning(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   SPS30Component *sps30_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sps30 | ||||||
|  | }  // namespace esphome | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import i2c, sensor, sensirion_common | from esphome.components import i2c, sensor, sensirion_common | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.automation import maybe_simple_id | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_PM_1_0, |     CONF_PM_1_0, | ||||||
| @@ -25,6 +27,7 @@ from esphome.const import ( | |||||||
|     ICON_RULER, |     ICON_RULER, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@martgras"] | ||||||
| DEPENDENCIES = ["i2c"] | DEPENDENCIES = ["i2c"] | ||||||
| AUTO_LOAD = ["sensirion_common"] | AUTO_LOAD = ["sensirion_common"] | ||||||
|  |  | ||||||
| @@ -33,6 +36,11 @@ SPS30Component = sps30_ns.class_( | |||||||
|     "SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice |     "SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | # Actions | ||||||
|  | StartFanAction = sps30_ns.class_("StartFanAction", automation.Action) | ||||||
|  |  | ||||||
|  | CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -100,6 +108,7 @@ CONFIG_SCHEMA = ( | |||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.polling_component_schema("60s")) |     .extend(cv.polling_component_schema("60s")) | ||||||
| @@ -151,3 +160,21 @@ async def to_code(config): | |||||||
|     if CONF_PM_SIZE in config: |     if CONF_PM_SIZE in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_PM_SIZE]) |         sens = await sensor.new_sensor(config[CONF_PM_SIZE]) | ||||||
|         cg.add(var.set_pm_size_sensor(sens)) |         cg.add(var.set_pm_size_sensor(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_AUTO_CLEANING_INTERVAL in config: | ||||||
|  |         cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL])) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SPS30_ACTION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(SPS30Component), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def sps30_fan_to_code(config, action_id, template_arg, args): | ||||||
|  |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|  |     return cg.new_Pvariable(action_id, template_arg, paren) | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #include "sps30.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include "sps30.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace sps30 { | namespace sps30 { | ||||||
| @@ -44,6 +45,22 @@ void SPS30Component::setup() { | |||||||
|       this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); |       this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); | ||||||
|     } |     } | ||||||
|     ESP_LOGD(TAG, "  Serial Number: '%s'", this->serial_number_); |     ESP_LOGD(TAG, "  Serial Number: '%s'", this->serial_number_); | ||||||
|  |  | ||||||
|  |     bool result; | ||||||
|  |     if (this->fan_interval_.has_value()) { | ||||||
|  |       // override default value | ||||||
|  |       result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); | ||||||
|  |     } else { | ||||||
|  |       result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); | ||||||
|  |     } | ||||||
|  |     if (result) { | ||||||
|  |       delay(20); | ||||||
|  |       uint16_t secs[2]; | ||||||
|  |       if (this->read_data(secs, 2)) { | ||||||
|  |         fan_interval_ = secs[0] << 16 | secs[1]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this->status_clear_warning(); |     this->status_clear_warning(); | ||||||
|     this->skipped_data_read_cycles_ = 0; |     this->skipped_data_read_cycles_ = 0; | ||||||
|     this->start_continuous_measurement_(); |     this->start_continuous_measurement_(); | ||||||
| @@ -206,5 +223,16 @@ bool SPS30Component::start_continuous_measurement_() { | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool SPS30Component::start_fan_cleaning() { | ||||||
|  |   if (!write_command(SPS30_CMD_START_FAN_CLEANING)) { | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); | ||||||
|  |     return false; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGD(TAG, "Fan auto clean started"); | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace sps30 | }  // namespace sps30 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -22,12 +22,14 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri | |||||||
|   void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } |   void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } | ||||||
|  |  | ||||||
|   void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } |   void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } | ||||||
|  |   void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; } | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void update() override; |   void update() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   float get_setup_priority() const override { return setup_priority::DATA; } |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |  | ||||||
|  |   bool start_fan_cleaning(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   char serial_number_[17] = {0};  /// Terminating NULL character |   char serial_number_[17] = {0};  /// Terminating NULL character | ||||||
|   uint16_t raw_firmware_version_; |   uint16_t raw_firmware_version_; | ||||||
| @@ -54,6 +56,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri | |||||||
|   sensor::Sensor *pmc_4_0_sensor_{nullptr}; |   sensor::Sensor *pmc_4_0_sensor_{nullptr}; | ||||||
|   sensor::Sensor *pmc_10_0_sensor_{nullptr}; |   sensor::Sensor *pmc_10_0_sensor_{nullptr}; | ||||||
|   sensor::Sensor *pm_size_sensor_{nullptr}; |   sensor::Sensor *pm_size_sensor_{nullptr}; | ||||||
|  |   optional<uint32_t> fan_interval_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace sps30 | }  // namespace sps30 | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user