diff --git a/.clang-tidy b/.clang-tidy index fc5ce854f1..473529a9b1 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -14,7 +14,12 @@ Checks: >- -cert-str34-c, -clang-analyzer-optin.cplusplus.UninitializedObject, -clang-analyzer-osx.*, + -clang-diagnostic-delete-abstract-non-virtual-dtor, + -clang-diagnostic-delete-non-abstract-non-virtual-dtor, -clang-diagnostic-shadow-field, + -clang-diagnostic-sign-compare, + -clang-diagnostic-unused-variable, + -clang-diagnostic-unused-const-variable, -cppcoreguidelines-avoid-c-arrays, -cppcoreguidelines-avoid-goto, -cppcoreguidelines-avoid-magic-numbers, @@ -80,7 +85,6 @@ Checks: >- -readability-use-anyofallof, -warnings-as-errors WarningsAsErrors: '*' -HeaderFilterRegex: '^.*/src/esphome/.*' AnalyzeTemporaryDtors: false FormatStyle: google CheckOptions: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ccaaa47b5..d7a841f84c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,26 +36,27 @@ jobs: container: ghcr.io/esphome/esphome-lint:1.1 steps: - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" echo "::add-matcher::.github/workflows/matchers/gcc.json" + # Also run git-diff-index so that the step is marked as failed on formatting errors, + # since clang-format doesn't do anything but change files if -i is passed. - name: Run clang-format - run: script/clang-format -i + run: | + script/clang-format -i + git diff-index --quiet HEAD -- if: ${{ matrix.id == 'clang-format' }} - name: Run clang-tidy run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} if: ${{ matrix.id == 'clang-tidy' }} - - name: Suggest changes + - name: Suggested changes run: script/ci-suggest-changes + if: always() ci: # Don't use the esphome-lint docker image because it may contain outdated requirements. @@ -82,6 +83,9 @@ jobs: - id: test file: tests/test4.yaml name: Test tests/test4.yaml + - id: test + file: tests/test5.yaml + name: Test tests/test5.yaml - id: pytest name: Run pytest diff --git a/.gitignore b/.gitignore index 954ecb2cb8..92d92e4b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,5 @@ config/ tests/build/ tests/.esphome/ /.temp-clang-tidy.cpp +/.temp/ .pio/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8c55646f28..b6584bc735 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,7 +10,7 @@ { "label": "clang-tidy", "type": "shell", - "command": "test -f .gcc-flags.json || pio init --silent --ide atom; ./script/clang-tidy", + "command": "./script/clang-tidy", "problemMatcher": [ { "owner": "clang-tidy", diff --git a/CODEOWNERS b/CODEOWNERS index 557fe7cc08..1298d4d43d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,6 +14,8 @@ esphome/core/* @esphome/core esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/addressable_light/* @justfalter +esphome/components/am43/* @buxtronix +esphome/components/am43/cover/* @buxtronix esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter @@ -29,6 +31,7 @@ esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet +esphome/components/color_temperature/* @jesserockz esphome/components/coolix/* @glmnet esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun @@ -37,6 +40,7 @@ esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee +esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_improv/* @jesserockz @@ -48,7 +52,9 @@ esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/havells_solar/* @sourabhjaiswal +esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter +esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core esphome/components/improv/* @jesserockz esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -83,19 +89,26 @@ esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter +esphome/components/pipsolar/* @andreashergert1984 +esphome/components/pmsa003i/* @sjtrny esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core esphome/components/pulse_meter/* @stevebaxter +esphome/components/pvvx_mithermometer/* @pasiz esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz +esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces +esphome/components/sdp3x/* @Azimath +esphome/components/selec_meter/* @sourabhjaiswal +esphome/components/select/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny @@ -119,14 +132,19 @@ esphome/components/st7789v/* @kbx81 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core +esphome/components/t6615/* @tylermenezes esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter +esphome/components/tlc5947/* @rnauber esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage +esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka +esphome/components/toshiba/* @kbx81 +esphome/components/tsl2591/* @wjcarpenter esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/sensor/* @jesserockz diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh deleted file mode 100755 index d9a80cde2e..0000000000 --- a/docker/rootfs/etc/cont-init.d/30-esphome.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# This files installs the user ESPHome version if specified -# ============================================================================== - -declare esphome_version - -if bashio::config.has_value 'esphome_version'; then - esphome_version=$(bashio::config 'esphome_version') - if [[ $esphome_version == *":"* ]]; then - IFS=':' read -r -a array <<< "$esphome_version" - username=${array[0]} - ref=${array[1]} - else - username="esphome" - ref=$esphome_version - fi - full_url="https://github.com/${username}/esphome/archive/${ref}.zip" - bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." - pip3 install -U --no-cache-dir "${full_url}" \ - || bashio::exit.nok "Failed installing esphome pinned version." -fi diff --git a/docker/rootfs/etc/cont-init.d/40-migrate.sh b/docker/rootfs/etc/cont-init.d/40-migrate.sh deleted file mode 100755 index 88e8be26b9..0000000000 --- a/docker/rootfs/etc/cont-init.d/40-migrate.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/with-contenv bashio -# ============================================================================== -# Community Hass.io Add-ons: ESPHome -# This files migrates the esphome config directory from the old path -# ============================================================================== - -if [[ ! -d /config/esphome && -d /config/esphomeyaml ]]; then - echo "Moving config directory from /config/esphomeyaml to /config/esphome" - mv /config/esphomeyaml /config/esphome - mv /config/esphome/.esphomeyaml /config/esphome/.esphome -fi diff --git a/esphome/__main__.py b/esphome/__main__.py index a7a3836b69..8d6f2b8f89 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -11,6 +11,7 @@ from esphome.config import iter_components, read_config, strip_default_ids from esphome.const import ( CONF_BAUD_RATE, CONF_BROKER, + CONF_DEASSERT_RTS_DTR, CONF_LOGGER, CONF_OTA, CONF_PASSWORD, @@ -99,10 +100,21 @@ def run_miniterm(config, port): baud_rate = config["logger"][CONF_BAUD_RATE] if baud_rate == 0: _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.") + return _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) backtrace_state = False - with serial.Serial(port, baudrate=baud_rate) as ser: + ser = serial.Serial() + ser.baudrate = baud_rate + ser.port = port + + # We can't set to False by default since it leads to toggling and hence + # ESP32 resets on some platforms. + if config["logger"][CONF_DEASSERT_RTS_DTR]: + ser.dtr = False + ser.rts = False + + with ser: while True: try: raw = ser.readline() @@ -284,7 +296,6 @@ def command_vscode(args): logging.disable(logging.INFO) logging.disable(logging.WARNING) - CORE.config_path = args.configuration vscode.read_config(args) @@ -394,7 +405,7 @@ def command_update_all(args): import click success = {} - files = list_yaml_files(args.configuration[0]) + files = list_yaml_files(args.configuration) twidth = 60 def print_bar(middle_text): @@ -408,7 +419,7 @@ def command_update_all(args): print("-" * twidth) print() rc = run_external_process( - "esphome", "--dashboard", "run", "--no-logs", "--device", "OTA", f + "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" ) if rc == 0: print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) @@ -505,6 +516,7 @@ def parse_args(argv): "clean", "dashboard", "vscode", + "update-all", ], ) @@ -681,14 +693,12 @@ def parse_args(argv): ) parser_vscode = subparsers.add_parser("vscode") - parser_vscode.add_argument( - "configuration", help="Your YAML configuration file.", nargs=1 - ) + parser_vscode.add_argument("configuration", help="Your YAML configuration file.") parser_vscode.add_argument("--ace", action="store_true") parser_update = subparsers.add_parser("update-all") parser_update.add_argument( - "configuration", help="Your YAML configuration file directory.", nargs=1 + "configuration", help="Your YAML configuration file directories.", nargs="+" ) return parser.parse_args(argv[1:]) diff --git a/esphome/boards.py b/esphome/boards.py new file mode 100644 index 0000000000..220d440a37 --- /dev/null +++ b/esphome/boards.py @@ -0,0 +1,875 @@ +ESP8266_BASE_PINS = { + "A0": 17, + "SS": 15, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "SDA": 4, + "SCL": 5, + "RX": 3, + "TX": 1, +} + +ESP8266_BOARD_PINS = { + "d1": { + "D0": 3, + "D1": 1, + "D2": 16, + "D3": 5, + "D4": 4, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 0, + "D9": 2, + "D10": 15, + "D11": 13, + "D12": 14, + "D13": 14, + "D14": 4, + "D15": 5, + "LED": 2, + }, + "d1_mini": { + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "LED": 2, + }, + "d1_mini_lite": "d1_mini", + "d1_mini_pro": "d1_mini", + "esp01": {}, + "esp01_1m": {}, + "esp07": {}, + "esp12e": {}, + "esp210": {}, + "esp8285": {}, + "esp_wroom_02": {}, + "espduino": {"LED": 16}, + "espectro": {"LED": 15, "BUTTON": 2}, + "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, + "espinotee": {"LED": 16}, + "espresso_lite_v1": {"LED": 16}, + "espresso_lite_v2": {"LED": 2}, + "gen4iod": {}, + "heltec_wifi_kit_8": "d1_mini", + "huzzah": { + "LED": 0, + "LED_RED": 0, + "LED_BLUE": 2, + "D4": 4, + "D5": 5, + "D12": 12, + "D13": 13, + "D14": 14, + "D15": 15, + "D16": 16, + }, + "inventone": {}, + "modwifi": {}, + "nodemcu": { + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "D10": 1, + "LED": 16, + }, + "nodemcuv2": "nodemcu", + "oak": { + "P0": 2, + "P1": 5, + "P2": 0, + "P3": 3, + "P4": 1, + "P5": 4, + "P6": 15, + "P7": 13, + "P8": 12, + "P9": 14, + "P10": 16, + "P11": 17, + "LED": 5, + }, + "phoenix_v1": {"LED": 16}, + "phoenix_v2": {"LED": 2}, + "sparkfunBlynk": "thing", + "thing": {"LED": 5, "SDA": 2, "SCL": 14}, + "thingdev": "thing", + "wifi_slot": {"LED": 2}, + "wifiduino": { + "D0": 3, + "D1": 1, + "D2": 2, + "D3": 0, + "D4": 4, + "D5": 5, + "D6": 16, + "D7": 14, + "D8": 12, + "D9": 13, + "D10": 15, + "D11": 13, + "D12": 12, + "D13": 14, + }, + "wifinfo": { + "LED": 12, + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "D10": 1, + }, + "wio_link": {"LED": 2, "GROVE": 15, "D0": 14, "D1": 12, "D2": 13, "BUTTON": 0}, + "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, + "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, +} + +FLASH_SIZE_1_MB = 2 ** 20 +FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 +FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB +FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB +FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB + +ESP8266_FLASH_SIZES = { + "d1": FLASH_SIZE_4_MB, + "d1_mini": FLASH_SIZE_4_MB, + "d1_mini_lite": FLASH_SIZE_1_MB, + "d1_mini_pro": FLASH_SIZE_16_MB, + "esp01": FLASH_SIZE_512_KB, + "esp01_1m": FLASH_SIZE_1_MB, + "esp07": FLASH_SIZE_4_MB, + "esp12e": FLASH_SIZE_4_MB, + "esp210": FLASH_SIZE_4_MB, + "esp8285": FLASH_SIZE_1_MB, + "esp_wroom_02": FLASH_SIZE_2_MB, + "espduino": FLASH_SIZE_4_MB, + "espectro": FLASH_SIZE_4_MB, + "espino": FLASH_SIZE_4_MB, + "espinotee": FLASH_SIZE_4_MB, + "espresso_lite_v1": FLASH_SIZE_4_MB, + "espresso_lite_v2": FLASH_SIZE_4_MB, + "gen4iod": FLASH_SIZE_512_KB, + "heltec_wifi_kit_8": FLASH_SIZE_4_MB, + "huzzah": FLASH_SIZE_4_MB, + "inventone": FLASH_SIZE_4_MB, + "modwifi": FLASH_SIZE_2_MB, + "nodemcu": FLASH_SIZE_4_MB, + "nodemcuv2": FLASH_SIZE_4_MB, + "oak": FLASH_SIZE_4_MB, + "phoenix_v1": FLASH_SIZE_4_MB, + "phoenix_v2": FLASH_SIZE_4_MB, + "sparkfunBlynk": FLASH_SIZE_4_MB, + "thing": FLASH_SIZE_512_KB, + "thingdev": FLASH_SIZE_512_KB, + "wifi_slot": FLASH_SIZE_1_MB, + "wifiduino": FLASH_SIZE_4_MB, + "wifinfo": FLASH_SIZE_1_MB, + "wio_link": FLASH_SIZE_4_MB, + "wio_node": FLASH_SIZE_4_MB, + "xinabox_cw01": FLASH_SIZE_4_MB, +} + +ESP8266_LD_SCRIPTS = { + FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), + FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), + FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), + FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), + FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), +} + +ESP32_BASE_PINS = { + "TX": 1, + "RX": 3, + "SDA": 21, + "SCL": 22, + "SS": 5, + "MOSI": 23, + "MISO": 19, + "SCK": 18, + "A0": 36, + "A3": 39, + "A4": 32, + "A5": 33, + "A6": 34, + "A7": 35, + "A10": 4, + "A11": 0, + "A12": 2, + "A13": 15, + "A14": 13, + "A15": 12, + "A16": 14, + "A17": 27, + "A18": 25, + "A19": 26, + "T0": 4, + "T1": 0, + "T2": 2, + "T3": 15, + "T4": 13, + "T5": 12, + "T6": 14, + "T7": 27, + "T8": 33, + "T9": 32, + "DAC1": 25, + "DAC2": 26, + "SVP": 36, + "SVN": 39, +} + +ESP32_BOARD_PINS = { + "alksesp32": { + "A0": 32, + "A1": 33, + "A2": 25, + "A3": 26, + "A4": 27, + "A5": 14, + "A6": 12, + "A7": 15, + "D0": 40, + "D1": 41, + "D10": 19, + "D11": 21, + "D12": 22, + "D13": 23, + "D2": 15, + "D3": 2, + "D4": 0, + "D5": 4, + "D6": 16, + "D7": 17, + "D8": 5, + "D9": 18, + "DHT_PIN": 26, + "LED": 23, + "L_B": 5, + "L_G": 17, + "L_R": 22, + "L_RGB_B": 16, + "L_RGB_G": 21, + "L_RGB_R": 4, + "L_Y": 23, + "MISO": 22, + "MOSI": 21, + "PHOTO": 25, + "PIEZO1": 19, + "PIEZO2": 18, + "POT1": 32, + "POT2": 33, + "S1": 4, + "S2": 16, + "S3": 18, + "S4": 19, + "S5": 21, + "SCK": 23, + "SCL": 14, + "SDA": 27, + "SS": 19, + "SW1": 15, + "SW2": 2, + "SW3": 0, + }, + "bpi-bit": { + "BUTTON_A": 35, + "BUTTON_B": 27, + "BUZZER": 25, + "LIGHT_SENSOR1": 36, + "LIGHT_SENSOR2": 39, + "MPU9250_INT": 0, + "P0": 25, + "P1": 32, + "P10": 26, + "P11": 27, + "P12": 2, + "P13": 18, + "P14": 19, + "P15": 23, + "P16": 5, + "P19": 22, + "P2": 33, + "P20": 21, + "P3": 13, + "P4": 15, + "P5": 35, + "P6": 12, + "P7": 14, + "P8": 16, + "P9": 17, + "RGB_LED": 4, + "TEMPERATURE_SENSOR": 34, + }, + "d-duino-32": { + "D1": 5, + "D10": 1, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "MISO": 12, + "MOSI": 13, + "SCK": 14, + "SCL": 4, + "SDA": 5, + "SS": 15, + }, + "esp-wrover-kit": {}, + "esp32-devkitlipo": {}, + "esp32-evb": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + "SS": 17, + }, + "esp32-gateway": {"BUTTON": 34, "LED": 33, "SCL": 16, "SDA": 32}, + "esp32-poe-iso": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + }, + "esp32-poe": {"BUTTON": 34, "MISO": 15, "MOSI": 2, "SCK": 14, "SCL": 16, "SDA": 13}, + "esp32-pro": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + "SS": 17, + }, + "esp320": { + "LED": 5, + "MISO": 12, + "MOSI": 13, + "SCK": 14, + "SCL": 14, + "SDA": 2, + "SS": 15, + }, + "esp32cam": {}, + "esp32dev": {}, + "esp32doit-devkit-v1": {"LED": 2}, + "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, + "esp32vn-iot-uno": {}, + "espea32": {"BUTTON": 0, "LED": 5}, + "espectro32": {"LED": 15, "SD_SS": 33}, + "espino32": {"BUTTON": 0, "LED": 16}, + "featheresp32": { + "A0": 26, + "A1": 25, + "A10": 27, + "A11": 12, + "A12": 13, + "A13": 35, + "A2": 34, + "A4": 36, + "A5": 4, + "A6": 14, + "A7": 32, + "A8": 15, + "A9": 33, + "Ax": 2, + "LED": 13, + "MOSI": 18, + "RX": 16, + "SCK": 5, + "SDA": 23, + "SS": 33, + "TX": 17, + }, + "firebeetle32": {"LED": 2}, + "fm-devkit": { + "D0": 34, + "D1": 35, + "D10": 0, + "D2": 32, + "D3": 33, + "D4": 27, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 23, + "I2S_DOUT": 22, + "I2S_LRCLK": 25, + "I2S_MCLK": 2, + "I2S_SCLK": 26, + "LED": 5, + "SCL": 17, + "SDA": 16, + "SW1": 4, + "SW2": 18, + "SW3": 19, + "SW4": 21, + }, + "frogboard": {}, + "heltec_wifi_kit_32": { + "A1": 37, + "A2": 38, + "BUTTON": 0, + "LED": 25, + "RST_OLED": 16, + "SCL_OLED": 15, + "SDA_OLED": 4, + "Vext": 21, + }, + "heltec_wifi_lora_32": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 33, + "DIO2": 32, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wifi_lora_32_V2": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wireless_stick": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "hornbill32dev": {"BUTTON": 0, "LED": 13}, + "hornbill32minima": {"SS": 2}, + "intorobot": { + "A1": 39, + "A2": 35, + "A3": 25, + "A4": 26, + "A5": 14, + "A6": 12, + "A7": 15, + "A8": 13, + "A9": 2, + "BUTTON": 0, + "D0": 19, + "D1": 23, + "D2": 18, + "D3": 17, + "D4": 16, + "D5": 5, + "D6": 4, + "LED": 4, + "MISO": 17, + "MOSI": 16, + "RGB_B_BUILTIN": 22, + "RGB_G_BUILTIN": 21, + "RGB_R_BUILTIN": 27, + "SCL": 19, + "SDA": 23, + "T0": 19, + "T1": 23, + "T2": 18, + "T3": 17, + "T4": 16, + "T5": 5, + "T6": 4, + }, + "iotaap_magnolia": {}, + "iotbusio": {}, + "iotbusproteus": {}, + "lolin32": {"LED": 5}, + "lolin32_lite": {"LED": 22}, + "lolin_d32": {"LED": 5, "_VBAT": 35}, + "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, + "lopy": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 17, + }, + "lopy4": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 18, + }, + "m5stack-core-esp32": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + "RXD2": 16, + "TXD2": 17, + }, + "m5stack-fire": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + }, + "m5stack-grey": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + "RXD2": 16, + "TXD2": 17, + }, + "m5stick-c": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G10": 10, + "G26": 26, + "G32": 32, + "G33": 33, + "G36": 36, + "G37": 37, + "G39": 39, + "G9": 9, + "MISO": 36, + "MOSI": 15, + "SCK": 13, + "SCL": 33, + "SDA": 32, + }, + "magicbit": { + "BLUE_LED": 17, + "BUZZER": 25, + "GREEN_LED": 16, + "LDR": 36, + "LED": 16, + "LEFT_BUTTON": 35, + "MOTOR1A": 27, + "MOTOR1B": 18, + "MOTOR2A": 16, + "MOTOR2B": 17, + "POT": 39, + "RED_LED": 27, + "RIGHT_PUTTON": 34, + "YELLOW_LED": 18, + }, + "mhetesp32devkit": {"LED": 2}, + "mhetesp32minikit": {"LED": 2}, + "microduino-core-esp32": { + "A0": 12, + "A1": 13, + "A10": 25, + "A11": 26, + "A12": 27, + "A13": 14, + "A2": 15, + "A3": 4, + "A6": 38, + "A7": 37, + "A8": 32, + "A9": 33, + "D0": 3, + "D1": 1, + "D10": 5, + "D11": 23, + "D12": 19, + "D13": 18, + "D14": 12, + "D15": 13, + "D16": 15, + "D17": 4, + "D18": 22, + "D19": 21, + "D2": 16, + "D20": 38, + "D21": 37, + "D3": 17, + "D4": 32, + "D5": 33, + "D6": 25, + "D7": 26, + "D8": 27, + "D9": 14, + "SCL": 21, + "SCL1": 13, + "SDA": 22, + "SDA1": 12, + }, + "nano32": {"BUTTON": 0, "LED": 16}, + "nina_w10": { + "D0": 3, + "D1": 1, + "D10": 5, + "D11": 19, + "D12": 23, + "D13": 18, + "D14": 13, + "D15": 12, + "D16": 32, + "D17": 33, + "D18": 21, + "D19": 34, + "D2": 26, + "D20": 36, + "D21": 39, + "D3": 25, + "D4": 35, + "D5": 27, + "D6": 22, + "D7": 0, + "D8": 15, + "D9": 14, + "LED_BLUE": 21, + "LED_GREEN": 33, + "LED_RED": 23, + "SCL": 13, + "SDA": 12, + "SW1": 33, + "SW2": 27, + }, + "node32s": {}, + "nodemcu-32s": {"BUTTON": 0, "LED": 2}, + "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, + "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, + "oroca_edubot": { + "A0": 34, + "A1": 39, + "A2": 36, + "A3": 33, + "D0": 4, + "D1": 16, + "D2": 17, + "D3": 22, + "D4": 23, + "D5": 5, + "D6": 18, + "D7": 19, + "D8": 33, + "LED": 13, + "MOSI": 18, + "RX": 16, + "SCK": 5, + "SDA": 23, + "SS": 2, + "TX": 17, + "VBAT": 35, + }, + "pico32": {}, + "pocket_32": {"LED": 16}, + "pycom_gpy": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 17, + }, + "quantum": {}, + "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, + "tinypico": {}, + "ttgo-lora32-v1": { + "A1": 37, + "A2": 38, + "BUTTON": 0, + "LED": 2, + "MOSI": 27, + "SCK": 5, + "SS": 18, + }, + "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, + "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, + "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, + "ttgo-t7-v13-mini32": {"LED": 22}, + "ttgo-t7-v14-mini32": {"LED": 19}, + "turta_iot_node": {}, + "vintlabs-devkit-v1": { + "LED": 2, + "PWM0": 12, + "PWM1": 13, + "PWM2": 14, + "PWM3": 15, + "PWM4": 16, + "PWM5": 17, + "PWM6": 18, + "PWM7": 19, + }, + "wemos_d1_mini32": { + "D0": 26, + "D1": 22, + "D2": 21, + "D3": 17, + "D4": 16, + "D5": 18, + "D6": 19, + "D7": 23, + "D8": 5, + "LED": 2, + "RXD": 3, + "TXD": 1, + "_VBAT": 35, + }, + "wemosbat": {"LED": 16}, + "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, + "widora-air": { + "A1": 39, + "A2": 35, + "A3": 25, + "A4": 26, + "A5": 14, + "A6": 12, + "A7": 15, + "A8": 13, + "A9": 2, + "BUTTON": 0, + "D0": 19, + "D1": 23, + "D2": 18, + "D3": 17, + "D4": 16, + "D5": 5, + "D6": 4, + "LED": 25, + "MISO": 17, + "MOSI": 16, + "SCL": 19, + "SDA": 23, + "T0": 19, + "T1": 23, + "T2": 18, + "T3": 17, + "T4": 16, + "T5": 5, + "T6": 4, + }, + "xinabox_cw02": {"LED": 27}, +} + +ESP32_C3_BASE_PINS = { + "TX": 21, + "RX": 20, + "ADC1_0": 0, + "ADC1_1": 1, + "ADC1_2": 2, + "ADC1_3": 3, + "ADC1_4": 4, + "ADC2_0": 5, +} + +ESP32_C3_BOARD_PINS = { + "esp32-c3-devkitm-1": {"LED": 8}, + "esp32-c3-devkitc-02": "esp32-c3-devkitm-1", +} diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index ad6018268e..7e9251ddf3 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -125,7 +125,7 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { } void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { - // Attaching pin interrupts on the same pin will override the previous interupt + // Attaching pin interrupts on the same pin will override the previous interrupt // However, the user expects that multiple dimmers sharing the same ZC pin will work. // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers // if any of them are using the same ZC pin, and also trigger the interrupt for *them*. diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 63fd7c60cc..5f60fbe0b2 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -42,7 +42,7 @@ void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { - it[led].set(COLOR_BLACK); + it[led].set(Color::BLACK); } } diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index d6469ab785..78f12a6b9e 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "adc"; void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } inline adc1_channel_t gpio_to_adc1(uint8_t pin) { +#if CONFIG_IDF_TARGET_ESP32 switch (pin) { case 36: return ADC1_CHANNEL_0; @@ -34,6 +35,22 @@ inline adc1_channel_t gpio_to_adc1(uint8_t pin) { default: return ADC1_CHANNEL_MAX; } +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + switch (pin) { + case 0: + return ADC1_CHANNEL_0; + case 1: + return ADC1_CHANNEL_1; + case 2: + return ADC1_CHANNEL_2; + case 3: + return ADC1_CHANNEL_3; + case 4: + return ADC1_CHANNEL_4; + default: + return ADC1_CHANNEL_MAX; + } +#endif } #endif @@ -46,8 +63,10 @@ void ADCSensor::setup() { #ifdef ARDUINO_ARCH_ESP32 adc1_config_channel_atten(gpio_to_adc1(pin_), attenuation_); adc1_config_width(ADC_WIDTH_BIT_12); +#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_)); #endif +#endif } void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); @@ -89,6 +108,7 @@ float ADCSensor::sample() { #ifdef ARDUINO_ARCH_ESP32 int raw = adc1_get_raw(gpio_to_adc1(pin_)); float value_v = raw / 4095.0f; +#if CONFIG_IDF_TARGET_ESP32 switch (this->attenuation_) { case ADC_ATTEN_DB_0: value_v *= 1.1; @@ -105,6 +125,24 @@ float ADCSensor::sample() { default: // This is to satisfy the unused ADC_ATTEN_MAX break; } +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + switch (this->attenuation_) { + case ADC_ATTEN_DB_0: + value_v *= 0.84; + break; + case ADC_ATTEN_DB_2_5: + value_v *= 1.13; + break; + case ADC_ATTEN_DB_6: + value_v *= 1.56; + break; + case ADC_ATTEN_DB_11: + value_v *= 3.0; + break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; + } +#endif return value_v; #endif diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 7a944a7260..7c32e4a923 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_PIN, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) @@ -37,7 +36,10 @@ ADCSensor = adc_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index 90873f1a5e..80dafe2417 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,27 +31,34 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(ADE7953), cv.Optional(CONF_IRQ_PIN): pins.input_pin, cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT_A): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT_B): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index c521769279..da33a39041 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_GAIN, CONF_MULTIPLEXER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, CONF_ID, @@ -53,7 +52,10 @@ ADS1115Sensor = ads1115_ns.class_( CONF_ADS1115_ID = "ads1115_id" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 4688440d80..da8c6e844e 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -10,7 +10,7 @@ // // According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost // immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best -// results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time. +// results making successive requests; the current implementation makes 3 attempts with a delay of 30ms each time. #include "aht10.h" #include "esphome/core/log.h" @@ -23,7 +23,7 @@ static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms -static const uint8_t AHT10_ATTEMPS = 3; // safety margin, normally 3 attemps are enough: 3*30=90ms +static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms void AHT10Component::setup() { ESP_LOGCONFIG(TAG, "Setting up AHT10..."); @@ -58,8 +58,8 @@ void AHT10Component::update() { uint8_t delay = AHT10_DEFAULT_DELAY; if (this->humidity_sensor_ != nullptr) delay = AHT10_HUMIDITY_DELAY; - for (int i = 0; i < AHT10_ATTEMPS; ++i) { - ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis()); + for (int i = 0; i < AHT10_ATTEMPTS; ++i) { + ESP_LOGVV(TAG, "Attempt %u at %6ld", i, millis()); delay_microseconds_accurate(4); if (!this->read_bytes(0, data, 6, delay)) { ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index 35168be54a..654d645966 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(AHT10Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/am2320/sensor.py b/esphome/components/am2320/sensor.py index 5d6cb9eded..088978a8f1 100644 --- a/esphome/components/am2320/sensor.py +++ b/esphome/components/am2320/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, ) @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(AM2320Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/am43/__init__.py b/esphome/components/am43/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/am43/am43.cpp b/esphome/components/am43/am43.cpp new file mode 100644 index 0000000000..e60bb2474c --- /dev/null +++ b/esphome/components/am43/am43.cpp @@ -0,0 +1,115 @@ +#include "am43.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace am43 { + +static const char *TAG = "am43"; + +void Am43::dump_config() { + ESP_LOGCONFIG(TAG, "AM43"); + LOG_SENSOR(" ", "Battery", this->battery_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +void Am43::setup() { + this->encoder_ = new Am43Encoder(); + this->decoder_ = new Am43Decoder(); + this->logged_in_ = false; + this->last_battery_update_ = 0; + this->current_sensor_ = 0; +} + +void Am43::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_OPEN_EVT: { + this->logged_in_ = false; + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->logged_in_ = false; + this->node_state = espbt::ClientState::Idle; + if (this->battery_ != nullptr) + this->battery_->publish_state(NAN); + if (this->illuminance_ != nullptr) + this->illuminance_->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); + if (chr == nullptr) { + if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", + this->parent_->address_str().c_str()); + } else { + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", + this->parent_->address_str().c_str()); + } + break; + } + this->char_handle_ = chr->handle; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::Established; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_) + break; + this->decoder_->decode(param->notify.value, param->notify.value_len); + + if (this->battery_ != nullptr && this->decoder_->has_battery_level() && + millis() - this->last_battery_update_ > 10000) { + this->battery_->publish_state(this->decoder_->battery_level_); + this->last_battery_update_ = millis(); + } + + if (this->illuminance_ != nullptr && this->decoder_->has_light_level()) { + this->illuminance_->publish_state(this->decoder_->light_level_); + } + + if (this->current_sensor_ > 0) { + if (this->illuminance_ != nullptr) { + auto packet = this->encoder_->get_light_level_request(); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), + status); + } + this->current_sensor_ = 0; + } + break; + } + default: + break; + } +} + +void Am43::update() { + if (this->node_state != espbt::ClientState::Established) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + return; + } + if (this->current_sensor_ == 0) { + if (this->battery_ != nullptr) { + auto packet = this->encoder_->get_battery_level_request(); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } + this->current_sensor_++; + } +} + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/am43.h b/esphome/components/am43/am43.h new file mode 100644 index 0000000000..460bb601b2 --- /dev/null +++ b/esphome/components/am43/am43.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/am43/am43_base.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +namespace esphome { +namespace am43 { + +namespace espbt = esphome::esp32_ble_tracker; + +class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() 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::DATA; } + void set_battery(sensor::Sensor *battery) { battery_ = battery; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + + protected: + uint16_t char_handle_; + Am43Encoder *encoder_; + Am43Decoder *decoder_; + bool logged_in_; + sensor::Sensor *battery_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; + uint8_t current_sensor_; + // The AM43 often gets into a state where it spams loads of battery update + // notifications. Here we will limit to no more than every 10s. + uint8_t last_battery_update_; +}; + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/am43_base.cpp b/esphome/components/am43/am43_base.cpp new file mode 100644 index 0000000000..a17df55571 --- /dev/null +++ b/esphome/components/am43/am43_base.cpp @@ -0,0 +1,142 @@ +#include "am43_base.h" + +namespace esphome { +namespace am43 { + +const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; + +std::string pkt_to_hex(const uint8_t *data, uint16_t len) { + char buf[64]; + memset(buf, 0, 64); + for (int i = 0; i < len; i++) + sprintf(&buf[i * 2], "%02x", data[i]); + std::string ret = buf; + return ret; +} + +Am43Packet *Am43Encoder::get_battery_level_request() { + uint8_t data = 0x1; + return this->encode_(0xA2, &data, 1); +} + +Am43Packet *Am43Encoder::get_light_level_request() { + uint8_t data = 0x1; + return this->encode_(0xAA, &data, 1); +} + +Am43Packet *Am43Encoder::get_position_request() { + uint8_t data = 0x1; + return this->encode_(CMD_GET_POSITION, &data, 1); +} + +Am43Packet *Am43Encoder::get_send_pin_request(uint16_t pin) { + uint8_t data[2]; + data[0] = (pin & 0xFF00) >> 8; + data[1] = pin & 0xFF; + return this->encode_(CMD_SEND_PIN, data, 2); +} + +Am43Packet *Am43Encoder::get_open_request() { + uint8_t data = 0xDD; + return this->encode_(CMD_SET_STATE, &data, 1); +} + +Am43Packet *Am43Encoder::get_close_request() { + uint8_t data = 0xEE; + return this->encode_(CMD_SET_STATE, &data, 1); +} + +Am43Packet *Am43Encoder::get_stop_request() { + uint8_t data = 0xCC; + return this->encode_(CMD_SET_STATE, &data, 1); +} + +Am43Packet *Am43Encoder::get_set_position_request(uint8_t position) { + return this->encode_(CMD_SET_POSITION, &position, 1); +} + +void Am43Encoder::checksum_() { + uint8_t checksum = 0; + int i = 0; + for (i = 0; i < this->packet_.length; i++) + checksum = checksum ^ this->packet_.data[i]; + this->packet_.data[i] = checksum ^ 0xff; + this->packet_.length++; +} + +Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) { + memcpy(this->packet_.data, START_PACKET, 5); + this->packet_.data[5] = command; + this->packet_.data[6] = length; + memcpy(&this->packet_.data[7], data, length); + this->packet_.length = length + 7; + this->checksum_(); + ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str()); + return &this->packet_; +} + +#define VERIFY_MIN_LENGTH(x) \ + if (length < (x)) \ + return; + +void Am43Decoder::decode(const uint8_t *data, uint16_t length) { + this->has_battery_level_ = false; + this->has_light_level_ = false; + this->has_set_position_response_ = false; + this->has_set_state_response_ = false; + this->has_position_ = false; + this->has_pin_response_ = false; + ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str()); + + if (length < 2 || data[0] != 0x9a) + return; + switch (data[1]) { + case CMD_GET_BATTERY_LEVEL: { + VERIFY_MIN_LENGTH(8); + this->battery_level_ = data[7]; + this->has_battery_level_ = true; + break; + } + case CMD_GET_LIGHT_LEVEL: { + VERIFY_MIN_LENGTH(5); + this->light_level_ = 100 * ((float) data[4] / 9); + this->has_light_level_ = true; + break; + } + case CMD_GET_POSITION: { + VERIFY_MIN_LENGTH(6); + this->position_ = data[5]; + this->has_position_ = true; + break; + } + case CMD_NOTIFY_POSITION: { + VERIFY_MIN_LENGTH(5); + this->position_ = data[4]; + this->has_position_ = true; + break; + } + case CMD_SEND_PIN: { + VERIFY_MIN_LENGTH(4); + this->pin_ok_ = data[3] == RESPONSE_ACK; + this->has_pin_response_ = true; + break; + } + case CMD_SET_POSITION: { + VERIFY_MIN_LENGTH(4); + this->set_position_ok_ = data[3] == RESPONSE_ACK; + this->has_set_position_response_ = true; + break; + } + case CMD_SET_STATE: { + VERIFY_MIN_LENGTH(4); + this->set_state_ok_ = data[3] == RESPONSE_ACK; + this->has_set_state_response_ = true; + break; + } + default: + break; + } +}; + +} // namespace am43 +} // namespace esphome diff --git a/esphome/components/am43/am43_base.h b/esphome/components/am43/am43_base.h new file mode 100644 index 0000000000..e817f161fe --- /dev/null +++ b/esphome/components/am43/am43_base.h @@ -0,0 +1,78 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace am43 { + +static const uint16_t AM43_SERVICE_UUID = 0xFE50; +static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51; +// +// Tuya identifiers, only to detect and warn users as they are incompatible. +static const uint16_t AM43_TUYA_SERVICE_UUID = 0x1910; +static const uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11; + +struct Am43Packet { + uint8_t length; + uint8_t data[24]; +}; + +static const uint8_t CMD_GET_BATTERY_LEVEL = 0xA2; +static const uint8_t CMD_GET_LIGHT_LEVEL = 0xAA; +static const uint8_t CMD_GET_POSITION = 0xA7; +static const uint8_t CMD_SEND_PIN = 0x17; +static const uint8_t CMD_SET_STATE = 0x0A; +static const uint8_t CMD_SET_POSITION = 0x0D; +static const uint8_t CMD_NOTIFY_POSITION = 0xA1; + +static const uint8_t RESPONSE_ACK = 0x5A; +static const uint8_t RESPONSE_NACK = 0xA5; + +class Am43Encoder { + public: + Am43Packet *get_battery_level_request(); + Am43Packet *get_light_level_request(); + Am43Packet *get_position_request(); + Am43Packet *get_send_pin_request(uint16_t pin); + Am43Packet *get_open_request(); + Am43Packet *get_close_request(); + Am43Packet *get_stop_request(); + Am43Packet *get_set_position_request(uint8_t position); + + protected: + void checksum_(); + Am43Packet *encode_(uint8_t command, uint8_t *data, uint8_t length); + Am43Packet packet_; +}; + +class Am43Decoder { + public: + void decode(const uint8_t *data, uint16_t length); + bool has_battery_level() { return this->has_battery_level_; } + bool has_light_level() { return this->has_light_level_; } + bool has_set_position_response() { return this->has_set_position_response_; } + bool has_set_state_response() { return this->has_set_state_response_; } + bool has_position() { return this->has_position_; } + bool has_pin_response() { return this->has_pin_response_; } + + union { + uint8_t position_; + uint8_t battery_level_; + float light_level_; + uint8_t set_position_ok_; + uint8_t set_state_ok_; + uint8_t pin_ok_; + }; + + protected: + bool has_battery_level_; + bool has_light_level_; + bool has_set_position_response_; + bool has_set_state_response_; + bool has_position_; + bool has_pin_response_; +}; + +} // namespace am43 +} // namespace esphome diff --git a/esphome/components/am43/cover/__init__.py b/esphome/components/am43/cover/__init__.py new file mode 100644 index 0000000000..1ab0edbe78 --- /dev/null +++ b/esphome/components/am43/cover/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover, ble_client +from esphome.const import CONF_ID, CONF_PIN + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["ble_client"] +AUTO_LOAD = ["am43"] + +CONF_INVERT_POSITION = "invert_position" + +am43_ns = cg.esphome_ns.namespace("am43") +Am43Component = am43_ns.class_( + "Am43Component", cover.Cover, ble_client.BLEClientNode, cg.Component +) + +CONFIG_SCHEMA = ( + cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Am43Component), + cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF), + cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) + yield cg.register_component(var, config) + yield cover.register_cover(var, config) + yield ble_client.register_ble_node(var, config) diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp new file mode 100644 index 0000000000..3ae7fe8f8c --- /dev/null +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -0,0 +1,149 @@ +#include "am43_cover.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace am43 { + +static const char *TAG = "am43_cover"; + +using namespace esphome::cover; + +void Am43Component::dump_config() { + LOG_COVER("", "AM43 Cover", this); + ESP_LOGCONFIG(TAG, " Device Pin: %d", this->pin_); + ESP_LOGCONFIG(TAG, " Invert Position: %d", (int) this->invert_position_); +} + +void Am43Component::setup() { + this->position = COVER_OPEN; + this->encoder_ = new Am43Encoder(); + this->decoder_ = new Am43Decoder(); + this->logged_in_ = false; +} + +void Am43Component::loop() { + if (this->node_state == espbt::ClientState::Established && !this->logged_in_) { + auto packet = this->encoder_->get_send_pin_request(this->pin_); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str()); + if (status) + ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status); + else + this->logged_in_ = true; + } +} + +CoverTraits Am43Component::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_position(true); + traits.set_supports_tilt(false); + traits.set_is_assumed_state(false); + return traits; +} + +void Am43Component::control(const CoverCall &call) { + if (this->node_state != espbt::ClientState::Established) { + ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", this->get_name().c_str()); + return; + } + if (call.get_stop()) { + auto packet = this->encoder_->get_stop_request(); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); + } + if (call.get_position().has_value()) { + auto pos = *call.get_position(); + + if (this->invert_position_) + pos = 1 - pos; + auto packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100)); + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, + packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", this->get_name().c_str(), status); + } +} + +void Am43Component::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: { + this->logged_in_ = false; + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); + if (chr == nullptr) { + if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->get_name().c_str()); + } else { + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->get_name().c_str()); + } + break; + } + this->char_handle_ = chr->handle; + + auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::Established; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_) + break; + this->decoder_->decode(param->notify.value, param->notify.value_len); + + if (this->decoder_->has_position()) { + this->position = ((float) this->decoder_->position_ / 100.0); + if (!this->invert_position_) + this->position = 1 - this->position; + if (this->position > 0.97) + this->position = 1.0; + if (this->position < 0.02) + this->position = 0.0; + this->publish_state(); + } + + if (this->decoder_->has_pin_response()) { + if (this->decoder_->pin_ok_) { + ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str()); + auto packet = this->encoder_->get_position_request(); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", this->get_name().c_str(), status); + } else { + ESP_LOGW(TAG, "[%s] AM43 pin rejected!", this->get_name().c_str()); + } + } + + if (this->decoder_->has_set_position_response() && !this->decoder_->set_position_ok_) + ESP_LOGW(TAG, "[%s] Got nack after sending set_position. Bad pin?", this->get_name().c_str()); + + if (this->decoder_->has_set_state_response() && !this->decoder_->set_state_ok_) + ESP_LOGW(TAG, "[%s] Got nack after sending set_state. Bad pin?", this->get_name().c_str()); + break; + } + default: + break; + } +} + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h new file mode 100644 index 0000000000..5557da49e7 --- /dev/null +++ b/esphome/components/am43/cover/am43_cover.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/cover/cover.h" +#include "esphome/components/am43/am43_base.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +namespace esphome { +namespace am43 { + +namespace espbt = esphome::esp32_ble_tracker; + +class Am43Component : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component { + public: + void setup() override; + void loop() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + cover::CoverTraits get_traits() override; + void set_pin(uint16_t pin) { this->pin_ = pin; } + void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } + + protected: + void control(const cover::CoverCall &call) override; + uint16_t char_handle_; + uint16_t pin_; + bool invert_position_; + Am43Encoder *encoder_; + Am43Decoder *decoder_; + bool logged_in_; + + float position_; +}; + +} // namespace am43 +} // namespace esphome + +#endif diff --git a/esphome/components/am43/sensor.py b/esphome/components/am43/sensor.py new file mode 100644 index 0000000000..c88e529a0c --- /dev/null +++ b/esphome/components/am43/sensor.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client +from esphome.const import ( + CONF_ID, + CONF_BATTERY_LEVEL, + ICON_BATTERY, + CONF_ILLUMINANCE, + ICON_BRIGHTNESS_5, + UNIT_PERCENT, +) + +CODEOWNERS = ["@buxtronix"] + +am43_ns = cg.esphome_ns.namespace("am43") +Am43 = am43_ns.class_("Am43", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Am43), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + UNIT_PERCENT, ICON_BATTERY, 0 + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + UNIT_PERCENT, ICON_BRIGHTNESS_5, 0 + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("120s")) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield ble_client.register_ble_node(var, config) + + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery(sens)) + + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 63e0710936..f330969c33 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -90,19 +90,24 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ if (this->codec_->has_running()) { this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; } + if (this->codec_->has_unit()) { + this->fahrenheit_ = (this->codec_->unit_ == 'f'); + ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius"); + this->current_request_++; + } this->publish_state(); - if (this->current_request_ > 0) { + if (this->current_request_ > 1) { AnovaPacket *pkt = nullptr; switch (this->current_request_++) { - case 1: + case 2: pkt = this->codec_->get_read_target_temp_request(); break; - case 2: + case 3: pkt = this->codec_->get_read_current_temp_request(); break; default: - this->current_request_ = 0; + this->current_request_ = 1; break; } if (pkt != nullptr) { @@ -121,12 +126,16 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } } +void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); } + void Anova::update() { if (this->node_state != espbt::ClientState::Established) return; - if (this->current_request_ == 0) { + if (this->current_request_ < 2) { auto pkt = this->codec_->get_read_device_status_request(); + if (this->current_request_ == 0) + auto pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 63d03cb329..42bdbcaed0 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -36,12 +36,14 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode traits.set_visual_temperature_step(0.1); return traits; } + void set_unit_of_measurement(const char *); protected: AnovaCodec *codec_; void control(const climate::ClimateCall &call) override; uint16_t char_handle_; uint8_t current_request_; + bool fahrenheit_; }; } // namespace anova diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index 8cbc481643..ad581b1e6c 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -3,6 +3,10 @@ namespace esphome { namespace anova { +float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); } + +float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; } + AnovaPacket *AnovaCodec::clean_packet_() { this->packet_.length = strlen((char *) this->packet_.data); this->packet_.data[this->packet_.length] = '\0'; @@ -42,6 +46,8 @@ AnovaPacket *AnovaCodec::get_read_data_request() { AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) { this->current_query_ = SET_TARGET_TEMPERATURE; + if (this->fahrenheit_) + temperature = ctof(temperature); sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature); return this->clean_packet_(); } @@ -67,7 +73,6 @@ AnovaPacket *AnovaCodec::get_stop_request() { void AnovaCodec::decode(const uint8_t *data, uint16_t length) { memset(this->buf_, 0, 32); strncpy(this->buf_, (char *) data, length); - ESP_LOGV("anova", "Received: %s\n", this->buf_); this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false; switch (this->current_query_) { case READ_DEVICE_STATUS: { @@ -97,19 +102,32 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { } case READ_TARGET_TEMPERATURE: { this->target_temp_ = strtof(this->buf_, nullptr); + if (this->fahrenheit_) + this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case SET_TARGET_TEMPERATURE: { this->target_temp_ = strtof(this->buf_, nullptr); + if (this->fahrenheit_) + this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case READ_CURRENT_TEMPERATURE: { this->current_temp_ = strtof(this->buf_, nullptr); + if (this->fahrenheit_) + this->current_temp_ = ftoc(this->current_temp_); this->has_current_temp_ = true; break; } + case SET_UNIT: + case READ_UNIT: { + this->unit_ = this->buf_[0]; + this->fahrenheit_ = this->buf_[0] == 'f'; + this->has_unit_ = true; + break; + } default: break; } diff --git a/esphome/components/anova/anova_base.h b/esphome/components/anova/anova_base.h index e94fe619a6..7c1383512d 100644 --- a/esphome/components/anova/anova_base.h +++ b/esphome/components/anova/anova_base.h @@ -71,6 +71,7 @@ class AnovaCodec { bool has_unit_; bool has_running_; char buf_[32]; + bool fahrenheit_; CurrentQuery current_query_; }; diff --git a/esphome/components/anova/climate.py b/esphome/components/anova/climate.py index ab1c9045d8..bdd77d6a33 100644 --- a/esphome/components/anova/climate.py +++ b/esphome/components/anova/climate.py @@ -1,7 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate, ble_client -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT + +UNITS = { + "f": "f", + "c": "c", +} CODEOWNERS = ["@buxtronix"] DEPENDENCIES = ["ble_client"] @@ -12,14 +17,20 @@ Anova = anova_ns.class_( ) CONFIG_SCHEMA = ( - climate.CLIMATE_SCHEMA.extend({cv.GenerateID(): cv.declare_id(Anova)}) + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Anova), + cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), + } + ) .extend(ble_client.BLE_CLIENT_SCHEMA) .extend(cv.polling_component_schema("60s")) ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - yield ble_client.register_ble_node(var, config) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) diff --git a/esphome/components/apds9960/sensor.py b/esphome/components/apds9960/sensor.py index cb0c52735d..e1990ec26e 100644 --- a/esphome/components/apds9960/sensor.py +++ b/esphome/components/apds9960/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_TYPE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_LIGHTBULB, @@ -21,7 +20,10 @@ TYPES = { } CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 073775ed2e..b9b56d3f8e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -39,6 +39,7 @@ service APIConnection { rpc camera_image (CameraImageRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {} rpc number_command (NumberCommandRequest) returns (void) {} + rpc select_command (SelectCommandRequest) returns (void) {} } @@ -213,6 +214,7 @@ message ListEntitiesBinarySensorResponse { string device_class = 5; bool is_status_binary_sensor = 6; + bool disabled_by_default = 7; } message BinarySensorStateResponse { option (id) = 21; @@ -242,6 +244,7 @@ message ListEntitiesCoverResponse { bool supports_position = 6; bool supports_tilt = 7; string device_class = 8; + bool disabled_by_default = 9; } enum LegacyCoverState { @@ -309,6 +312,7 @@ message ListEntitiesFanResponse { bool supports_speed = 6; bool supports_direction = 7; int32 supported_speed_count = 8; + bool disabled_by_default = 9; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -352,6 +356,18 @@ message FanCommandRequest { } // ==================== LIGHT ==================== +enum ColorMode { + COLOR_MODE_UNKNOWN = 0; + COLOR_MODE_ON_OFF = 1; + COLOR_MODE_BRIGHTNESS = 2; + COLOR_MODE_WHITE = 7; + COLOR_MODE_COLOR_TEMPERATURE = 11; + COLOR_MODE_COLD_WARM_WHITE = 19; + COLOR_MODE_RGB = 35; + COLOR_MODE_RGB_WHITE = 39; + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; + COLOR_MODE_RGB_COLD_WARM_WHITE = 51; +} message ListEntitiesLightResponse { option (id) = 15; option (source) = SOURCE_SERVER; @@ -362,13 +378,16 @@ message ListEntitiesLightResponse { string name = 3; string unique_id = 4; - bool supports_brightness = 5; - bool supports_rgb = 6; - bool supports_white_value = 7; - bool supports_color_temperature = 8; + repeated ColorMode supported_color_modes = 12; + // next four supports_* are for legacy clients, newer clients should use color modes + bool legacy_supports_brightness = 5 [deprecated=true]; + bool legacy_supports_rgb = 6 [deprecated=true]; + bool legacy_supports_white_value = 7 [deprecated=true]; + bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; repeated string effects = 11; + bool disabled_by_default = 13; } message LightStateResponse { option (id) = 24; @@ -379,12 +398,15 @@ message LightStateResponse { fixed32 key = 1; bool state = 2; float brightness = 3; + ColorMode color_mode = 11; float color_brightness = 10; float red = 4; float green = 5; float blue = 6; float white = 7; float color_temperature = 8; + float cold_white = 12; + float warm_white = 13; string effect = 9; } message LightCommandRequest { @@ -398,6 +420,8 @@ message LightCommandRequest { bool state = 3; bool has_brightness = 4; float brightness = 5; + bool has_color_mode = 22; + ColorMode color_mode = 23; bool has_color_brightness = 20; float color_brightness = 21; bool has_rgb = 6; @@ -408,6 +432,10 @@ message LightCommandRequest { float white = 11; bool has_color_temperature = 12; float color_temperature = 13; + bool has_cold_white = 24; + float cold_white = 25; + bool has_warm_white = 26; + float warm_white = 27; bool has_transition_length = 14; uint32 transition_length = 15; bool has_flash_length = 16; @@ -445,6 +473,7 @@ message ListEntitiesSensorResponse { string device_class = 9; SensorStateClass state_class = 10; SensorLastResetType last_reset_type = 11; + bool disabled_by_default = 12; } message SensorStateResponse { option (id) = 25; @@ -472,6 +501,7 @@ message ListEntitiesSwitchResponse { string icon = 5; bool assumed_state = 6; + bool disabled_by_default = 7; } message SwitchStateResponse { option (id) = 26; @@ -504,6 +534,7 @@ message ListEntitiesTextSensorResponse { string unique_id = 4; string icon = 5; + bool disabled_by_default = 6; } message TextSensorStateResponse { option (id) = 27; @@ -663,6 +694,7 @@ message ListEntitiesCameraResponse { fixed32 key = 2; string name = 3; string unique_id = 4; + bool disabled_by_default = 5; } message CameraImageResponse { @@ -755,6 +787,7 @@ message ListEntitiesClimateResponse { repeated string supported_custom_fan_modes = 15; repeated ClimatePreset supported_presets = 16; repeated string supported_custom_presets = 17; + bool disabled_by_default = 18; } message ClimateStateResponse { option (id) = 47; @@ -822,6 +855,7 @@ message ListEntitiesNumberResponse { float min_value = 6; float max_value = 7; float step = 8; + bool disabled_by_default = 9; } message NumberStateResponse { option (id) = 50; @@ -844,3 +878,40 @@ message NumberCommandRequest { fixed32 key = 1; float state = 2; } + +// ==================== SELECT ==================== +message ListEntitiesSelectResponse { + option (id) = 52; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + repeated string options = 6; + bool disabled_by_default = 7; +} +message SelectStateResponse { + option (id) = 53; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the select does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; +} +message SelectCommandRequest { + option (id) = 54; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fb05772e5e..5fba549a57 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -176,6 +176,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); msg.device_class = binary_sensor->get_device_class(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); + msg.disabled_by_default = binary_sensor->is_disabled_by_default(); return this->send_list_entities_binary_sensor_response(msg); } #endif @@ -207,6 +208,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.device_class = cover->get_device_class(); + msg.disabled_by_default = cover->is_disabled_by_default(); return this->send_list_entities_cover_response(msg); } void APIConnection::cover_command(const CoverCommandRequest &msg) { @@ -268,6 +270,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); + msg.disabled_by_default = fan->is_disabled_by_default(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -301,22 +304,28 @@ bool APIConnection::send_light_state(light::LightState *light) { auto traits = light->get_traits(); auto values = light->remote_values; + auto color_mode = values.get_color_mode(); LightStateResponse resp{}; resp.key = light->get_object_id_hash(); resp.state = values.is_on(); - if (traits.get_supports_brightness()) + resp.color_mode = static_cast(color_mode); + if (color_mode & light::ColorCapability::BRIGHTNESS) resp.brightness = values.get_brightness(); - if (traits.get_supports_rgb()) { + if (color_mode & light::ColorCapability::RGB) { resp.color_brightness = values.get_color_brightness(); resp.red = values.get_red(); resp.green = values.get_green(); resp.blue = values.get_blue(); } - if (traits.get_supports_rgb_white_value()) + if (color_mode & light::ColorCapability::WHITE) resp.white = values.get_white(); - if (traits.get_supports_color_temperature()) + if (color_mode & light::ColorCapability::COLOR_TEMPERATURE) resp.color_temperature = values.get_color_temperature(); + if (color_mode & light::ColorCapability::COLD_WARM_WHITE) { + resp.cold_white = values.get_cold_white(); + resp.warm_white = values.get_warm_white(); + } if (light->supports_effects()) resp.effect = light->get_effect_name(); return this->send_light_state_response(resp); @@ -328,11 +337,21 @@ bool APIConnection::send_light_info(light::LightState *light) { msg.object_id = light->get_object_id(); msg.name = light->get_name(); msg.unique_id = get_default_unique_id("light", light); - msg.supports_brightness = traits.get_supports_brightness(); - msg.supports_rgb = traits.get_supports_rgb(); - msg.supports_white_value = traits.get_supports_rgb_white_value(); - msg.supports_color_temperature = traits.get_supports_color_temperature(); - if (msg.supports_color_temperature) { + + msg.disabled_by_default = light->is_disabled_by_default(); + + for (auto mode : traits.get_supported_color_modes()) + msg.supported_color_modes.push_back(static_cast(mode)); + + msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); + msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); + msg.legacy_supports_white_value = + msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); + msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); + + if (msg.legacy_supports_color_temperature) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } @@ -353,6 +372,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_state(msg.state); if (msg.has_brightness) call.set_brightness(msg.brightness); + if (msg.has_color_mode) + call.set_color_mode(static_cast(msg.color_mode)); if (msg.has_color_brightness) call.set_color_brightness(msg.color_brightness); if (msg.has_rgb) { @@ -364,6 +385,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_white(msg.white); if (msg.has_color_temperature) call.set_color_temperature(msg.color_temperature); + if (msg.has_cold_white) + call.set_cold_white(msg.cold_white); + if (msg.has_warm_white) + call.set_warm_white(msg.warm_white); if (msg.has_transition_length) call.set_transition_length(msg.transition_length); if (msg.has_flash_length) @@ -400,6 +425,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.device_class = sensor->get_device_class(); msg.state_class = static_cast(sensor->state_class); msg.last_reset_type = static_cast(sensor->last_reset_type); + msg.disabled_by_default = sensor->is_disabled_by_default(); return this->send_list_entities_sensor_response(msg); } @@ -423,6 +449,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { msg.unique_id = get_default_unique_id("switch", a_switch); msg.icon = a_switch->get_icon(); msg.assumed_state = a_switch->assumed_state(); + msg.disabled_by_default = a_switch->is_disabled_by_default(); return this->send_list_entities_switch_response(msg); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { @@ -457,6 +484,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) if (msg.unique_id.empty()) msg.unique_id = get_default_unique_id("text_sensor", text_sensor); msg.icon = text_sensor->get_icon(); + msg.disabled_by_default = text_sensor->is_disabled_by_default(); return this->send_list_entities_text_sensor_response(msg); } #endif @@ -500,6 +528,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.object_id = climate->get_object_id(); msg.name = climate->get_name(); msg.unique_id = get_default_unique_id("climate", climate); + + msg.disabled_by_default = climate->is_disabled_by_default(); + msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); @@ -572,6 +603,7 @@ bool APIConnection::send_number_info(number::Number *number) { msg.name = number->get_name(); msg.unique_id = get_default_unique_id("number", number); msg.icon = number->traits.get_icon(); + msg.disabled_by_default = number->is_disabled_by_default(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); @@ -590,6 +622,42 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { } #endif +#ifdef USE_SELECT +bool APIConnection::send_select_state(select::Select *select, std::string state) { + if (!this->state_subscription_) + return false; + + SelectStateResponse resp{}; + resp.key = select->get_object_id_hash(); + resp.state = std::move(state); + resp.missing_state = !select->has_state(); + return this->send_select_state_response(resp); +} +bool APIConnection::send_select_info(select::Select *select) { + ListEntitiesSelectResponse msg; + msg.key = select->get_object_id_hash(); + msg.object_id = select->get_object_id(); + msg.name = select->get_name(); + msg.unique_id = get_default_unique_id("select", select); + msg.icon = select->traits.get_icon(); + msg.disabled_by_default = select->is_disabled_by_default(); + + for (const auto &option : select->traits.get_options()) + msg.options.push_back(option); + + return this->send_list_entities_select_response(msg); +} +void APIConnection::select_command(const SelectCommandRequest &msg) { + select::Select *select = App.get_select_by_key(msg.key); + if (select == nullptr) + return; + + auto call = select->make_call(); + call.set_option(msg.state); + call.perform(); +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) @@ -604,6 +672,7 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { msg.object_id = camera->get_object_id(); msg.name = camera->get_name(); msg.unique_id = get_default_unique_id("camera", camera); + msg.disabled_by_default = camera->is_disabled_by_default(); return this->send_list_entities_camera_response(msg); } void APIConnection::camera_image(const CameraImageRequest &msg) { @@ -655,7 +724,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 5; + resp.api_version_minor = 6; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 1d7fc48563..bc9839a423 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -67,6 +67,11 @@ class APIConnection : public APIServerConnection { bool send_number_state(number::Number *number, float state); bool send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; +#endif +#ifdef USE_SELECT + bool send_select_state(select::Select *select, std::string state); + bool send_select_info(select::Select *select); + void select_command(const SelectCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 057d71324f..eecba7a68e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -62,6 +62,32 @@ template<> const char *proto_enum_to_string(enums::FanDirec return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::ColorMode value) { + switch (value) { + case enums::COLOR_MODE_UNKNOWN: + return "COLOR_MODE_UNKNOWN"; + case enums::COLOR_MODE_ON_OFF: + return "COLOR_MODE_ON_OFF"; + case enums::COLOR_MODE_BRIGHTNESS: + return "COLOR_MODE_BRIGHTNESS"; + case enums::COLOR_MODE_WHITE: + return "COLOR_MODE_WHITE"; + case enums::COLOR_MODE_COLOR_TEMPERATURE: + return "COLOR_MODE_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_COLD_WARM_WHITE: + return "COLOR_MODE_COLD_WARM_WHITE"; + case enums::COLOR_MODE_RGB: + return "COLOR_MODE_RGB"; + case enums::COLOR_MODE_RGB_WHITE: + return "COLOR_MODE_RGB_WHITE"; + case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: + return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: + return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: @@ -235,6 +261,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } } void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HelloRequest::dump_to(std::string &out) const { char buffer[64]; out.append("HelloRequest {\n"); @@ -243,6 +270,7 @@ void HelloRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -272,6 +300,7 @@ void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HelloResponse::dump_to(std::string &out) const { char buffer[64]; out.append("HelloResponse {\n"); @@ -290,6 +319,7 @@ void HelloResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -301,6 +331,7 @@ bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value } } void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ConnectRequest::dump_to(std::string &out) const { char buffer[64]; out.append("ConnectRequest {\n"); @@ -309,6 +340,7 @@ void ConnectRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -320,6 +352,7 @@ bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { } } void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ConnectResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ConnectResponse {\n"); @@ -328,16 +361,27 @@ void ConnectResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } +#endif void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } +#endif void PingRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } +#endif void PingResponse::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } +#endif void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } +#endif bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -397,6 +441,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); } +#ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { char buffer[64]; out.append("DeviceInfoResponse {\n"); @@ -437,18 +482,29 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } +#endif void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } +#endif void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } +#endif bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { this->is_status_binary_sensor = value.as_bool(); return true; } + case 7: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -492,7 +548,9 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); + buffer.encode_bool(7, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesBinarySensorResponse {\n"); @@ -520,8 +578,13 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append(" is_status_binary_sensor: "); out.append(YESNO(this->is_status_binary_sensor)); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -551,6 +614,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void BinarySensorStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("BinarySensorStateResponse {\n"); @@ -568,6 +632,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -582,6 +647,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_tilt = value.as_bool(); return true; } + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -627,7 +696,9 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); buffer.encode_string(8, this->device_class); + buffer.encode_bool(9, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesCoverResponse {\n"); @@ -663,8 +734,13 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -704,6 +780,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(4, this->tilt); buffer.encode_enum(5, this->current_operation); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CoverStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("CoverStateResponse {\n"); @@ -731,6 +808,7 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -785,6 +863,7 @@ void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->tilt); buffer.encode_bool(8, this->stop); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CoverCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("CoverCommandRequest {\n"); @@ -824,6 +903,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -842,6 +922,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supported_speed_count = value.as_int32(); return true; } + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -883,7 +967,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); buffer.encode_int32(8, this->supported_speed_count); + buffer.encode_bool(9, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesFanResponse {\n"); @@ -920,8 +1006,13 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { sprintf(buffer, "%d", this->supported_speed_count); out.append(buffer); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -966,6 +1057,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); } +#ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("FanStateResponse {\n"); @@ -996,6 +1088,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1065,6 +1158,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(10, this->has_speed_level); buffer.encode_int32(11, this->speed_level); } +#ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("FanCommandRequest {\n"); @@ -1115,22 +1209,31 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { + case 12: { + this->supported_color_modes.push_back(value.as_enum()); + return true; + } case 5: { - this->supports_brightness = value.as_bool(); + this->legacy_supports_brightness = value.as_bool(); return true; } case 6: { - this->supports_rgb = value.as_bool(); + this->legacy_supports_rgb = value.as_bool(); return true; } case 7: { - this->supports_white_value = value.as_bool(); + this->legacy_supports_white_value = value.as_bool(); return true; } case 8: { - this->supports_color_temperature = value.as_bool(); + this->legacy_supports_color_temperature = value.as_bool(); + return true; + } + case 13: { + this->disabled_by_default = value.as_bool(); return true; } default: @@ -1182,16 +1285,21 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); - buffer.encode_bool(5, this->supports_brightness); - buffer.encode_bool(6, this->supports_rgb); - buffer.encode_bool(7, this->supports_white_value); - buffer.encode_bool(8, this->supports_color_temperature); + for (auto &it : this->supported_color_modes) { + buffer.encode_enum(12, it, true); + } + buffer.encode_bool(5, this->legacy_supports_brightness); + buffer.encode_bool(6, this->legacy_supports_rgb); + buffer.encode_bool(7, this->legacy_supports_white_value); + buffer.encode_bool(8, this->legacy_supports_color_temperature); buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); for (auto &it : this->effects) { buffer.encode_string(11, it, true); } + buffer.encode_bool(13, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesLightResponse {\n"); @@ -1212,20 +1320,26 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(this->unique_id).append("'"); out.append("\n"); - out.append(" supports_brightness: "); - out.append(YESNO(this->supports_brightness)); + for (const auto &it : this->supported_color_modes) { + out.append(" supported_color_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + out.append(" legacy_supports_brightness: "); + out.append(YESNO(this->legacy_supports_brightness)); out.append("\n"); - out.append(" supports_rgb: "); - out.append(YESNO(this->supports_rgb)); + out.append(" legacy_supports_rgb: "); + out.append(YESNO(this->legacy_supports_rgb)); out.append("\n"); - out.append(" supports_white_value: "); - out.append(YESNO(this->supports_white_value)); + out.append(" legacy_supports_white_value: "); + out.append(YESNO(this->legacy_supports_white_value)); out.append("\n"); - out.append(" supports_color_temperature: "); - out.append(YESNO(this->supports_color_temperature)); + out.append(" legacy_supports_color_temperature: "); + out.append(YESNO(this->legacy_supports_color_temperature)); out.append("\n"); out.append(" min_mireds: "); @@ -1243,14 +1357,23 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->state = value.as_bool(); return true; } + case 11: { + this->color_mode = value.as_enum(); + return true; + } default: return false; } @@ -1299,6 +1422,14 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 12: { + this->cold_white = value.as_float(); + return true; + } + case 13: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1307,14 +1438,18 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); + buffer.encode_enum(11, this->color_mode); buffer.encode_float(10, this->color_brightness); buffer.encode_float(4, this->red); buffer.encode_float(5, this->green); buffer.encode_float(6, this->blue); buffer.encode_float(7, this->white); buffer.encode_float(8, this->color_temperature); + buffer.encode_float(12, this->cold_white); + buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); } +#ifdef HAS_PROTO_MESSAGE_DUMP void LightStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("LightStateResponse {\n"); @@ -1332,6 +1467,10 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + out.append(" color_brightness: "); sprintf(buffer, "%g", this->color_brightness); out.append(buffer); @@ -1362,11 +1501,22 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" effect: "); out.append("'").append(this->effect).append("'"); out.append("\n"); out.append("}"); } +#endif bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1381,6 +1531,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_brightness = value.as_bool(); return true; } + case 22: { + this->has_color_mode = value.as_bool(); + return true; + } + case 23: { + this->color_mode = value.as_enum(); + return true; + } case 20: { this->has_color_brightness = value.as_bool(); return true; @@ -1397,6 +1555,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_color_temperature = value.as_bool(); return true; } + case 24: { + this->has_cold_white = value.as_bool(); + return true; + } + case 26: { + this->has_warm_white = value.as_bool(); + return true; + } case 14: { this->has_transition_length = value.as_bool(); return true; @@ -1465,6 +1631,14 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 25: { + this->cold_white = value.as_float(); + return true; + } + case 27: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1475,6 +1649,8 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->state); buffer.encode_bool(4, this->has_brightness); buffer.encode_float(5, this->brightness); + buffer.encode_bool(22, this->has_color_mode); + buffer.encode_enum(23, this->color_mode); buffer.encode_bool(20, this->has_color_brightness); buffer.encode_float(21, this->color_brightness); buffer.encode_bool(6, this->has_rgb); @@ -1485,6 +1661,10 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(11, this->white); buffer.encode_bool(12, this->has_color_temperature); buffer.encode_float(13, this->color_temperature); + buffer.encode_bool(24, this->has_cold_white); + buffer.encode_float(25, this->cold_white); + buffer.encode_bool(26, this->has_warm_white); + buffer.encode_float(27, this->warm_white); buffer.encode_bool(14, this->has_transition_length); buffer.encode_uint32(15, this->transition_length); buffer.encode_bool(16, this->has_flash_length); @@ -1492,6 +1672,7 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(18, this->has_effect); buffer.encode_string(19, this->effect); } +#ifdef HAS_PROTO_MESSAGE_DUMP void LightCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("LightCommandRequest {\n"); @@ -1517,6 +1698,14 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_color_mode: "); + out.append(YESNO(this->has_color_mode)); + out.append("\n"); + + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + out.append(" has_color_brightness: "); out.append(YESNO(this->has_color_brightness)); out.append("\n"); @@ -1563,6 +1752,24 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_cold_white: "); + out.append(YESNO(this->has_cold_white)); + out.append("\n"); + + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" has_warm_white: "); + out.append(YESNO(this->has_warm_white)); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" has_transition_length: "); out.append(YESNO(this->has_transition_length)); out.append("\n"); @@ -1590,6 +1797,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 7: { @@ -1608,6 +1816,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->last_reset_type = value.as_enum(); return true; } + case 12: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -1664,7 +1876,9 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(9, this->device_class); buffer.encode_enum(10, this->state_class); buffer.encode_enum(11, this->last_reset_type); + buffer.encode_bool(12, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesSensorResponse {\n"); @@ -1713,8 +1927,13 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" last_reset_type: "); out.append(proto_enum_to_string(this->last_reset_type)); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -1744,6 +1963,7 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SensorStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SensorStateResponse {\n"); @@ -1762,12 +1982,17 @@ void SensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { this->assumed_state = value.as_bool(); return true; } + case 7: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -1811,7 +2036,9 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->assumed_state); + buffer.encode_bool(7, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesSwitchResponse {\n"); @@ -1839,8 +2066,13 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append(" assumed_state: "); out.append(YESNO(this->assumed_state)); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1865,6 +2097,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SwitchStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SwitchStateResponse {\n"); @@ -1878,6 +2111,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1902,6 +2136,7 @@ void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SwitchCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("SwitchCommandRequest {\n"); @@ -1915,6 +2150,17 @@ void SwitchCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -1953,7 +2199,9 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesTextSensorResponse {\n"); @@ -1977,8 +2225,13 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -2014,6 +2267,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void TextSensorStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("TextSensorStateResponse {\n"); @@ -2031,6 +2285,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2049,6 +2304,7 @@ void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->level); buffer.encode_bool(2, this->dump_config); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeLogsRequest::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeLogsRequest {\n"); @@ -2061,6 +2317,7 @@ void SubscribeLogsRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2095,6 +2352,7 @@ void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(3, this->message); buffer.encode_bool(4, this->send_failed); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeLogsResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeLogsResponse {\n"); @@ -2115,10 +2373,13 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeassistantServicesRequest {}"); } +#endif bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2137,6 +2398,7 @@ void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HomeassistantServiceMap::dump_to(std::string &out) const { char buffer[64]; out.append("HomeassistantServiceMap {\n"); @@ -2149,6 +2411,7 @@ void HomeassistantServiceMap::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -2194,6 +2457,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(5, this->is_event); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HomeassistantServiceResponse::dump_to(std::string &out) const { char buffer[64]; out.append("HomeassistantServiceResponse {\n"); @@ -2224,10 +2488,13 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } +#endif bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2246,6 +2513,7 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); } +#ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("SubscribeHomeAssistantStateResponse {\n"); @@ -2258,6 +2526,7 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2281,6 +2550,7 @@ void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->state); buffer.encode_string(3, this->attribute); } +#ifdef HAS_PROTO_MESSAGE_DUMP void HomeAssistantStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("HomeAssistantStateResponse {\n"); @@ -2297,8 +2567,11 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } +#endif bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -2310,6 +2583,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } } void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } +#ifdef HAS_PROTO_MESSAGE_DUMP void GetTimeResponse::dump_to(std::string &out) const { char buffer[64]; out.append("GetTimeResponse {\n"); @@ -2319,6 +2593,7 @@ void GetTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2343,6 +2618,7 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); buffer.encode_enum(2, this->type); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesServicesArgument::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesServicesArgument {\n"); @@ -2355,6 +2631,7 @@ void ListEntitiesServicesArgument::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2386,6 +2663,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(3, it, true); } } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesServicesResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesServicesResponse {\n"); @@ -2405,6 +2683,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const { } out.append("}"); } +#endif bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2478,6 +2757,7 @@ void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(9, it, true); } } +#ifdef HAS_PROTO_MESSAGE_DUMP void ExecuteServiceArgument::dump_to(std::string &out) const { char buffer[64]; out.append("ExecuteServiceArgument {\n"); @@ -2531,6 +2811,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { } out.append("}"); } +#endif bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -2557,6 +2838,7 @@ void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(2, it, true); } } +#ifdef HAS_PROTO_MESSAGE_DUMP void ExecuteServiceRequest::dump_to(std::string &out) const { char buffer[64]; out.append("ExecuteServiceRequest {\n"); @@ -2572,6 +2854,17 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { } out.append("}"); } +#endif +bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2605,7 +2898,9 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); + buffer.encode_bool(5, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesCameraResponse {\n"); @@ -2625,8 +2920,13 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append(" unique_id: "); out.append("'").append(this->unique_id).append("'"); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -2662,6 +2962,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(2, this->data); buffer.encode_bool(3, this->done); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CameraImageResponse::dump_to(std::string &out) const { char buffer[64]; out.append("CameraImageResponse {\n"); @@ -2679,6 +2980,7 @@ void CameraImageResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -2697,6 +2999,7 @@ void CameraImageRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->single); buffer.encode_bool(2, this->stream); } +#ifdef HAS_PROTO_MESSAGE_DUMP void CameraImageRequest::dump_to(std::string &out) const { char buffer[64]; out.append("CameraImageRequest {\n"); @@ -2709,6 +3012,7 @@ void CameraImageRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -2743,6 +3047,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supported_presets.push_back(value.as_enum()); return true; } + case 18: { + this->disabled_by_default = value.as_bool(); + return true; + } default: return false; } @@ -2825,7 +3133,9 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_custom_presets) { buffer.encode_string(17, it, true); } + buffer.encode_bool(18, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesClimateResponse {\n"); @@ -2912,8 +3222,13 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2999,6 +3314,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(12, this->preset); buffer.encode_string(13, this->custom_preset); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ClimateStateResponse {\n"); @@ -3060,6 +3376,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -3185,6 +3502,7 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(20, this->has_custom_preset); buffer.encode_string(21, this->custom_preset); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("ClimateCommandRequest {\n"); @@ -3277,6 +3595,17 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 9: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} bool ListEntitiesNumberResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3330,7 +3659,9 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(6, this->min_value); buffer.encode_float(7, this->max_value); buffer.encode_float(8, this->step); + buffer.encode_bool(9, this->disabled_by_default); } +#ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesNumberResponse::dump_to(std::string &out) const { char buffer[64]; out.append("ListEntitiesNumberResponse {\n"); @@ -3369,8 +3700,13 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->step); out.append(buffer); out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); out.append("}"); } +#endif bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -3400,6 +3736,7 @@ void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void NumberStateResponse::dump_to(std::string &out) const { char buffer[64]; out.append("NumberStateResponse {\n"); @@ -3418,6 +3755,7 @@ void NumberStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -3436,6 +3774,7 @@ void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); } +#ifdef HAS_PROTO_MESSAGE_DUMP void NumberCommandRequest::dump_to(std::string &out) const { char buffer[64]; out.append("NumberCommandRequest {\n"); @@ -3450,6 +3789,194 @@ void NumberCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 7: { + this->disabled_by_default = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSelectResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 6: { + this->options.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesSelectResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + for (auto &it : this->options) { + buffer.encode_string(6, it, true); + } + buffer.encode_bool(7, this->disabled_by_default); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesSelectResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesSelectResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + for (const auto &it : this->options) { + out.append(" options: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + out.append("}"); +} +#endif +bool SelectStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SelectStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool SelectStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); + buffer.encode_bool(3, this->missing_state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SelectStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SelectStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + out.append("}"); +} +#endif +bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void SelectCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("SelectCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0551508b4b..be32488391 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -32,6 +32,18 @@ enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, }; +enum ColorMode : uint32_t { + COLOR_MODE_UNKNOWN = 0, + COLOR_MODE_ON_OFF = 1, + COLOR_MODE_BRIGHTNESS = 2, + COLOR_MODE_WHITE = 7, + COLOR_MODE_COLOR_TEMPERATURE = 11, + COLOR_MODE_COLD_WARM_WHITE = 19, + COLOR_MODE_RGB = 35, + COLOR_MODE_RGB_WHITE = 39, + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47, + COLOR_MODE_RGB_COLD_WARM_WHITE = 51, +}; enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, @@ -111,7 +123,9 @@ class HelloRequest : public ProtoMessage { public: std::string client_info{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -122,7 +136,9 @@ class HelloResponse : public ProtoMessage { uint32_t api_version_minor{0}; std::string server_info{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -132,7 +148,9 @@ class ConnectRequest : public ProtoMessage { public: std::string password{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -141,7 +159,9 @@ class ConnectResponse : public ProtoMessage { public: bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; @@ -149,35 +169,45 @@ class ConnectResponse : public ProtoMessage { class DisconnectRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; class DisconnectResponse : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; class PingRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; class PingResponse : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; class DeviceInfoRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -193,7 +223,9 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -202,21 +234,27 @@ class DeviceInfoResponse : public ProtoMessage { class ListEntitiesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; class ListEntitiesDoneResponse : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; class SubscribeStatesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -228,8 +266,11 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { std::string unique_id{}; std::string device_class{}; bool is_status_binary_sensor{false}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -242,7 +283,9 @@ class BinarySensorStateResponse : public ProtoMessage { bool state{false}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -258,8 +301,11 @@ class ListEntitiesCoverResponse : public ProtoMessage { bool supports_position{false}; bool supports_tilt{false}; std::string device_class{}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -274,7 +320,9 @@ class CoverStateResponse : public ProtoMessage { float tilt{0.0f}; enums::CoverOperation current_operation{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -291,7 +339,9 @@ class CoverCommandRequest : public ProtoMessage { float tilt{0.0f}; bool stop{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -307,8 +357,11 @@ class ListEntitiesFanResponse : public ProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -324,7 +377,9 @@ class FanStateResponse : public ProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -344,7 +399,9 @@ class FanCommandRequest : public ProtoMessage { bool has_speed_level{false}; int32_t speed_level{0}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -356,15 +413,19 @@ class ListEntitiesLightResponse : public ProtoMessage { uint32_t key{0}; std::string name{}; std::string unique_id{}; - bool supports_brightness{false}; - bool supports_rgb{false}; - bool supports_white_value{false}; - bool supports_color_temperature{false}; + std::vector supported_color_modes{}; + bool legacy_supports_brightness{false}; + bool legacy_supports_rgb{false}; + bool legacy_supports_white_value{false}; + bool legacy_supports_color_temperature{false}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -376,15 +437,20 @@ class LightStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; float brightness{0.0f}; + enums::ColorMode color_mode{}; float color_brightness{0.0f}; float red{0.0f}; float green{0.0f}; float blue{0.0f}; float white{0.0f}; float color_temperature{0.0f}; + float cold_white{0.0f}; + float warm_white{0.0f}; std::string effect{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -398,6 +464,8 @@ class LightCommandRequest : public ProtoMessage { bool state{false}; bool has_brightness{false}; float brightness{0.0f}; + bool has_color_mode{false}; + enums::ColorMode color_mode{}; bool has_color_brightness{false}; float color_brightness{0.0f}; bool has_rgb{false}; @@ -408,6 +476,10 @@ class LightCommandRequest : public ProtoMessage { float white{0.0f}; bool has_color_temperature{false}; float color_temperature{0.0f}; + bool has_cold_white{false}; + float cold_white{0.0f}; + bool has_warm_white{false}; + float warm_white{0.0f}; bool has_transition_length{false}; uint32_t transition_length{0}; bool has_flash_length{false}; @@ -415,7 +487,9 @@ class LightCommandRequest : public ProtoMessage { bool has_effect{false}; std::string effect{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -435,8 +509,11 @@ class ListEntitiesSensorResponse : public ProtoMessage { std::string device_class{}; enums::SensorStateClass state_class{}; enums::SensorLastResetType last_reset_type{}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -449,7 +526,9 @@ class SensorStateResponse : public ProtoMessage { float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -463,8 +542,11 @@ class ListEntitiesSwitchResponse : public ProtoMessage { std::string unique_id{}; std::string icon{}; bool assumed_state{false}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -476,7 +558,9 @@ class SwitchStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -487,7 +571,9 @@ class SwitchCommandRequest : public ProtoMessage { uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -500,12 +586,16 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { std::string name{}; std::string unique_id{}; std::string icon{}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class TextSensorStateResponse : public ProtoMessage { public: @@ -513,7 +603,9 @@ class TextSensorStateResponse : public ProtoMessage { std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -525,7 +617,9 @@ class SubscribeLogsRequest : public ProtoMessage { enums::LogLevel level{}; bool dump_config{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; @@ -537,7 +631,9 @@ class SubscribeLogsResponse : public ProtoMessage { std::string message{}; bool send_failed{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -546,7 +642,9 @@ class SubscribeLogsResponse : public ProtoMessage { class SubscribeHomeassistantServicesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -555,7 +653,9 @@ class HomeassistantServiceMap : public ProtoMessage { std::string key{}; std::string value{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -568,7 +668,9 @@ class HomeassistantServiceResponse : public ProtoMessage { std::vector variables{}; bool is_event{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -577,7 +679,9 @@ class HomeassistantServiceResponse : public ProtoMessage { class SubscribeHomeAssistantStatesRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -586,7 +690,9 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { std::string entity_id{}; std::string attribute{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -597,7 +703,9 @@ class HomeAssistantStateResponse : public ProtoMessage { std::string state{}; std::string attribute{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -605,7 +713,9 @@ class HomeAssistantStateResponse : public ProtoMessage { class GetTimeRequest : public ProtoMessage { public: void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: }; @@ -613,7 +723,9 @@ class GetTimeResponse : public ProtoMessage { public: uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -623,7 +735,9 @@ class ListEntitiesServicesArgument : public ProtoMessage { std::string name{}; enums::ServiceArgType type{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; @@ -635,7 +749,9 @@ class ListEntitiesServicesResponse : public ProtoMessage { uint32_t key{0}; std::vector args{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -653,7 +769,9 @@ class ExecuteServiceArgument : public ProtoMessage { std::vector float_array{}; std::vector string_array{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -665,7 +783,9 @@ class ExecuteServiceRequest : public ProtoMessage { uint32_t key{0}; std::vector args{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -677,12 +797,16 @@ class ListEntitiesCameraResponse : public ProtoMessage { uint32_t key{0}; std::string name{}; std::string unique_id{}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class CameraImageResponse : public ProtoMessage { public: @@ -690,7 +814,9 @@ class CameraImageResponse : public ProtoMessage { std::string data{}; bool done{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -702,7 +828,9 @@ class CameraImageRequest : public ProtoMessage { bool single{false}; bool stream{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; @@ -726,8 +854,11 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::vector supported_custom_fan_modes{}; std::vector supported_presets{}; std::vector supported_custom_presets{}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -750,7 +881,9 @@ class ClimateStateResponse : public ProtoMessage { enums::ClimatePreset preset{}; std::string custom_preset{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -781,7 +914,9 @@ class ClimateCommandRequest : public ProtoMessage { bool has_custom_preset{false}; std::string custom_preset{}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -798,12 +933,16 @@ class ListEntitiesNumberResponse : public ProtoMessage { float min_value{0.0f}; float max_value{0.0f}; float step{0.0f}; + bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class NumberStateResponse : public ProtoMessage { public: @@ -811,7 +950,9 @@ class NumberStateResponse : public ProtoMessage { float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; @@ -822,11 +963,60 @@ class NumberCommandRequest : public ProtoMessage { uint32_t key{0}; float state{0.0f}; void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; +#endif protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class ListEntitiesSelectResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + std::vector options{}; + bool disabled_by_default{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SelectStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + std::string state{}; + bool missing_state{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SelectCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + std::string state{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 440a5d0ab3..ad2413ea57 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -9,58 +9,82 @@ namespace api { static const char *const TAG = "api.service"; bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 2); } bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 4); } bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 5); } bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 6); } bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 7); } bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 8); } bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 10); } bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 19); } #ifdef USE_BINARY_SENSOR bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 12); } #endif #ifdef USE_BINARY_SENSOR bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 21); } #endif #ifdef USE_COVER bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 13); } #endif #ifdef USE_COVER bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 22); } #endif @@ -68,13 +92,17 @@ bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse #endif #ifdef USE_FAN bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 14); } #endif #ifdef USE_FAN bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 23); } #endif @@ -82,13 +110,17 @@ bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &ms #endif #ifdef USE_LIGHT bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 15); } #endif #ifdef USE_LIGHT bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 24); } #endif @@ -96,25 +128,33 @@ bool APIServerConnectionBase::send_light_state_response(const LightStateResponse #endif #ifdef USE_SENSOR bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 16); } #endif #ifdef USE_SENSOR bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 25); } #endif #ifdef USE_SWITCH bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 17); } #endif #ifdef USE_SWITCH bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 26); } #endif @@ -122,13 +162,17 @@ bool APIServerConnectionBase::send_switch_state_response(const SwitchStateRespon #endif #ifdef USE_TEXT_SENSOR bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 18); } #endif #ifdef USE_TEXT_SENSOR bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 27); } #endif @@ -136,35 +180,49 @@ bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsRe return this->send_message_(msg, 29); } bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 35); } bool APIServerConnectionBase::send_subscribe_home_assistant_state_response( const SubscribeHomeAssistantStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 39); } bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 36); } bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 37); } bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 41); } #ifdef USE_ESP32_CAMERA bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 43); } #endif #ifdef USE_ESP32_CAMERA bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 44); } #endif @@ -172,13 +230,17 @@ bool APIServerConnectionBase::send_camera_image_response(const CameraImageRespon #endif #ifdef USE_CLIMATE bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 46); } #endif #ifdef USE_CLIMATE bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 47); } #endif @@ -186,87 +248,129 @@ bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResp #endif #ifdef USE_NUMBER bool APIServerConnectionBase::send_list_entities_number_response(const ListEntitiesNumberResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_list_entities_number_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 49); } #endif #ifdef USE_NUMBER bool APIServerConnectionBase::send_number_state_response(const NumberStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "send_number_state_response: %s", msg.dump().c_str()); +#endif return this->send_message_(msg, 50); } #endif #ifdef USE_NUMBER #endif +#ifdef USE_SELECT +bool APIServerConnectionBase::send_list_entities_select_response(const ListEntitiesSelectResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_select_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 52); +} +#endif +#ifdef USE_SELECT +bool APIServerConnectionBase::send_select_state_response(const SelectStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_select_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 53); +} +#endif +#ifdef USE_SELECT +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { HelloRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str()); +#endif this->on_hello_request(msg); break; } case 3: { ConnectRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str()); +#endif this->on_connect_request(msg); break; } case 5: { DisconnectRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str()); +#endif this->on_disconnect_request(msg); break; } case 6: { DisconnectResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str()); +#endif this->on_disconnect_response(msg); break; } case 7: { PingRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str()); +#endif this->on_ping_request(msg); break; } case 8: { PingResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str()); +#endif this->on_ping_response(msg); break; } case 9: { DeviceInfoRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str()); +#endif this->on_device_info_request(msg); break; } case 11: { ListEntitiesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str()); +#endif this->on_list_entities_request(msg); break; } case 20: { SubscribeStatesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_states_request(msg); break; } case 28: { SubscribeLogsRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_logs_request(msg); break; } @@ -274,7 +378,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_COVER CoverCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); +#endif this->on_cover_command_request(msg); #endif break; @@ -283,7 +389,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_FAN FanCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); +#endif this->on_fan_command_request(msg); #endif break; @@ -292,7 +400,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_LIGHT LightCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); +#endif this->on_light_command_request(msg); #endif break; @@ -301,7 +411,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_SWITCH SwitchCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); +#endif this->on_switch_command_request(msg); #endif break; @@ -309,42 +421,54 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, case 34: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_homeassistant_services_request(msg); break; } case 36: { GetTimeRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); +#endif this->on_get_time_request(msg); break; } case 37: { GetTimeResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str()); +#endif this->on_get_time_response(msg); break; } case 38: { SubscribeHomeAssistantStatesRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str()); +#endif this->on_subscribe_home_assistant_states_request(msg); break; } case 40: { HomeAssistantStateResponse msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str()); +#endif this->on_home_assistant_state_response(msg); break; } case 42: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str()); +#endif this->on_execute_service_request(msg); break; } @@ -352,7 +476,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_ESP32_CAMERA CameraImageRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); +#endif this->on_camera_image_request(msg); #endif break; @@ -361,7 +487,9 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_CLIMATE ClimateCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); +#endif this->on_climate_command_request(msg); #endif break; @@ -370,8 +498,21 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_NUMBER NumberCommandRequest msg; msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); +#endif this->on_number_command_request(msg); +#endif + break; + } + case 54: { +#ifdef USE_SELECT + SelectCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); +#endif + this->on_select_command_request(msg); #endif break; } @@ -583,6 +724,19 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest & this->number_command(msg); } #endif +#ifdef USE_SELECT +void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->select_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 398c10a811..1b8d990b05 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -120,6 +120,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_NUMBER virtual void on_number_command_request(const NumberCommandRequest &value){}; +#endif +#ifdef USE_SELECT + bool send_list_entities_select_response(const ListEntitiesSelectResponse &msg); +#endif +#ifdef USE_SELECT + bool send_select_state_response(const SelectStateResponse &msg); +#endif +#ifdef USE_SELECT + virtual void on_select_command_request(const SelectCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -159,6 +168,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_NUMBER virtual void number_command(const NumberCommandRequest &msg) = 0; +#endif +#ifdef USE_SELECT + virtual void select_command(const SelectCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -194,6 +206,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_NUMBER void on_number_command_request(const NumberCommandRequest &msg) override; #endif +#ifdef USE_SELECT + void on_select_command_request(const SelectCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 7434030565..d48c0a4fd8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -206,6 +206,15 @@ void APIServer::on_number_update(number::Number *obj, float state) { } #endif +#ifdef USE_SELECT +void APIServer::on_select_update(select::Select *obj, const std::string &state) { + if (obj->is_internal()) + return; + for (auto *c : this->clients_) + c->send_select_state(obj, state); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index add22e121e..96b3192e9e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -9,7 +9,6 @@ #include "util.h" #include "list_entities.h" #include "subscribe_state.h" -#include "homeassistant_service.h" #include "user_services.h" #ifdef ARDUINO_ARCH_ESP32 @@ -63,6 +62,9 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_NUMBER void on_number_update(number::Number *obj, float state) override; +#endif +#ifdef USE_SELECT + void on_select_update(select::Select *obj, const std::string &state) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 8897758073..745dd92c89 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -55,5 +55,9 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this-> bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } #endif +#ifdef USE_SELECT +bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index c55ba5089e..c728fb0a97 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -42,6 +42,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_NUMBER bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; #endif bool on_end() override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 0d2eb2d279..0ba277d90a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -80,11 +80,13 @@ void ProtoMessage::decode(const uint8_t *buffer, size_t length) { } } +#ifdef HAS_PROTO_MESSAGE_DUMP std::string ProtoMessage::dump() const { std::string out; this->dump_to(out); return out; } +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index f1486a2511..dd054bab7e 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,8 +1,13 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/log.h" #include "esphome/core/helpers.h" +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define HAS_PROTO_MESSAGE_DUMP +#endif + namespace esphome { namespace api { @@ -243,8 +248,10 @@ class ProtoMessage { public: virtual void encode(ProtoWriteBuffer buffer) const = 0; void decode(const uint8_t *buffer, size_t length); +#ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; +#endif protected: virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 25aa7c8b31..1b8453f233 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -42,6 +42,11 @@ bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number, number->state); } #endif +#ifdef USE_SELECT +bool InitialStateIterator::on_select(select::Select *select) { + return this->client_->send_select_state(select, select->state); +} +#endif InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index f03322ac4a..beb9b947d4 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -39,6 +39,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_NUMBER bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 39e42bcc02..49618f5467 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -15,7 +15,7 @@ template<> std::string get_execute_arg_value(const ExecuteServiceAr template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { return arg.bool_array; } -template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { +template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { return arg.int_array; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index 6e05d49b74..5085994607 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -182,6 +182,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_SELECT + case IteratorState::SELECT: + if (this->at_ >= App.get_selects().size()) { + advance_platform = true; + } else { + auto *select = App.get_selects()[this->at_]; + if (select->is_internal()) { + success = true; + break; + } else { + success = this->on_select(select); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index f8b248056b..e404a95619 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -50,6 +50,9 @@ class ComponentIterator { #endif #ifdef USE_NUMBER virtual bool on_number(number::Number *number) = 0; +#endif +#ifdef USE_SELECT + virtual bool on_select(select::Select *select) = 0; #endif virtual bool on_end(); @@ -87,6 +90,9 @@ class ComponentIterator { #endif #ifdef USE_NUMBER NUMBER, +#endif +#ifdef USE_SELECT + SELECT, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index a571121742..271a29e0fc 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor from esphome.const import ( CONF_DISTANCE, CONF_LIGHTNING_ENERGY, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_KILOMETER, - UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH, ) @@ -19,14 +17,15 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( - UNIT_KILOMETER, - ICON_SIGNAL_DISTANCE_VARIANT, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOMETER, + icon=ICON_SIGNAL_DISTANCE_VARIANT, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( - UNIT_EMPTY, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index efa3f2b51a..0f6cc1abcb 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -34,28 +33,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(ATCMiThermometer), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index dc2048fbc2..7927a7fdfb 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -39,7 +39,7 @@ static const uint16_t ATM90E32_STATUS_S0_OVPHASEBST = 1 << 11; // Over voltage static const uint16_t ATM90E32_STATUS_S0_OVPHASECST = 1 << 10; // Over voltage on phase C static const uint16_t ATM90E32_STATUS_S0_UREVWNST = 1 << 9; // Voltage Phase Sequence Error status static const uint16_t ATM90E32_STATUS_S0_IREVWNST = 1 << 8; // Current Phase Sequence Error status -static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater tha INWarnTh reg +static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater than INWarnTh reg static const uint16_t ATM90E32_STATUS_S0_TQNOLOADST = 1 << 6; // All phase sum reactive power no-load condition status static const uint16_t ATM90E32_STATUS_S0_TPNOLOADST = 1 << 5; // All phase sum active power no-load condition status static const uint16_t ATM90E32_STATUS_S0_TASNOLOADST = 1 << 4; // All phase sum apparent power no-load status diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 2c34d76b52..28b49604ff 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -12,13 +12,11 @@ from esphome.const import ( CONF_FORWARD_ACTIVE_ENERGY, CONF_REVERSE_ACTIVE_ENERGY, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, ICON_LIGHTBULB, ICON_CURRENT_AC, LAST_RESET_TYPE_AUTO, @@ -27,7 +25,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT_HOURS, @@ -65,47 +62,47 @@ ATM90E32Component = atm90e32_ns.class_( ATM90E32_PHASE_SCHEMA = cv.Schema( { cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_EMPTY, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_LIGHTBULB, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + icon=ICON_LIGHTBULB, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER_FACTOR, - STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, @@ -120,18 +117,16 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 81c9243bdb..3098d462d2 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -47,7 +47,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { uint16_t battery_millivolt = data[2] << 8 | data[3]; float battery_voltage = battery_millivolt / 1000.0f; - // Temperature in 1000 * Celcius. + // Temperature in 1000 * Celsius. uint16_t temp_millicelcius = data[4] << 8 | data[5]; float temp_celcius = temp_millicelcius / 1000.0f; diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index d93e41816b..46ed64337f 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -33,28 +32,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(BParasite), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index e688241dcc..156c7bb375 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_RESOLUTION, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_LUX, CONF_MEASUREMENT_DURATION, @@ -28,7 +27,10 @@ BH1750Sensor = bh1750_ns.class_( CONF_MEASUREMENT_TIME = "measurement_time" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 731973bdad..86c83aff5c 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -12,7 +12,7 @@ class BinaryLightOutput : public light::LightOutput { void set_output(output::BinaryOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(false); + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 8f66978320..3c2169a922 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -6,6 +6,7 @@ from esphome.components import mqtt from esphome.const import ( CONF_DELAY, CONF_DEVICE_CLASS, + CONF_DISABLED_BY_DEFAULT, CONF_FILTERS, CONF_ID, CONF_INTERNAL, @@ -315,7 +316,7 @@ def validate_multi_click_timing(value): device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( +BINARY_SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(BinarySensor), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( @@ -377,6 +378,7 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( async def setup_binary_sensor_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_DEVICE_CLASS in config: diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 131a050052..946e2f9e62 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -7,8 +7,6 @@ from esphome.const import ( CONF_CHANNELS, CONF_VALUE, CONF_TYPE, - DEVICE_CLASS_EMPTY, - UNIT_EMPTY, ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP, @@ -35,11 +33,9 @@ entry = { CONFIG_SCHEMA = cv.typed_schema( { CONF_GROUP: sensor.sensor_schema( - UNIT_EMPTY, - ICON_CHECK_CIRCLE_OUTLINE, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_CHECK_CIRCLE_OUTLINE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ).extend( { cv.GenerateID(): cv.declare_id(BinarySensorMap), diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 203acc181f..b9d16ddacd 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -25,7 +25,7 @@ class BLEClientNode { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) = 0; - virtual void loop() = 0; + virtual void loop(){}; void set_address(uint64_t address) { address_ = address; } espbt::ESPBTClient *client; // This should be transitioned to Established once the node no longer needs diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index c6f05932ef..efe4bf0e9a 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -2,12 +2,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( - DEVICE_CLASS_EMPTY, CONF_ID, CONF_LAMBDA, STATE_CLASS_NONE, - UNIT_EMPTY, - ICON_EMPTY, CONF_TRIGGER_ID, CONF_SERVICE_UUID, ) @@ -34,7 +31,8 @@ BLESensorNotifyTrigger = ble_client_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index 819b7c6fd7..bca73328f9 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL, - ICON_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -20,11 +19,10 @@ BLERSSISensor = ble_rssi_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_DECIBEL, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py index 8c6cc7ae56..dcb842d879 100644 --- a/esphome/components/bme280/sensor.py +++ b/esphome/components/bme280/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_HECTOPASCAL, @@ -49,11 +48,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BME280Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -62,11 +60,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -75,11 +72,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index eaa158c9f8..76472c7562 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_OVERSAMPLING, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -20,7 +19,6 @@ from esphome.const import ( UNIT_OHM, ICON_GAS_CYLINDER, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, UNIT_PERCENT, ) @@ -59,11 +57,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BME680Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -72,11 +69,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -85,11 +81,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -98,11 +93,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( - UNIT_OHM, - ICON_GAS_CYLINDER, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( IIR_FILTER_OPTIONS, upper=True diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index 4520bf3480..8d00012150 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -6,13 +6,11 @@ from esphome.const import ( CONF_HUMIDITY, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - UNIT_EMPTY, UNIT_HECTOPASCAL, UNIT_OHM, UNIT_PARTS_PER_MILLION, @@ -54,54 +52,60 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_GAUGE, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( - UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IAQ): sensor.sensor_schema( - UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_TEST_TUBE, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_TEST_TUBE, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bmp085/sensor.py b/esphome/components/bmp085/sensor.py index 1b48f2e440..52f554120a 100644 --- a/esphome/components/bmp085/sensor.py +++ b/esphome/components/bmp085/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, ) @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BMP085Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bmp280/sensor.py b/esphome/components/bmp280/sensor.py index 48953d0259..95a9577f7e 100644 --- a/esphome/components/bmp280/sensor.py +++ b/esphome/components/bmp280/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, CONF_IIR_FILTER, CONF_OVERSAMPLING, @@ -46,11 +45,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BMP280Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -59,11 +57,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 4e81d6ac10..4c09a14c3e 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, @@ -28,18 +27,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(CCS811Component), cv.Required(CONF_ECO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( - UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 88991ff795..f6a9fa2927 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_AWAY, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, + CONF_DISABLED_BY_DEFAULT, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATURE, @@ -86,7 +87,7 @@ validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) -CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +CLIMATE_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(Climate), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), @@ -104,6 +105,7 @@ CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( async def setup_climate_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) visual = config[CONF_VISUAL] diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 690e81c250..b208e5946a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -63,9 +63,9 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); - ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead") + ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") ClimateCall &set_away(bool away); - ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead") + ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") ClimateCall &set_away(optional away); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); @@ -96,7 +96,7 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; - ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead") + ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20") optional get_away() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; @@ -193,7 +193,7 @@ class Climate : public Nameable { * Away allows climate devices to have two different target temperature configs: * one for normal mode and one for away mode. */ - ESPDEPRECATED("away is deprecated, use preset instead") + ESPDEPRECATED("away is deprecated, use preset instead", "v1.20") bool away{false}; /// The active fan mode of the climate device. diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index fbd6f158e6..48493b500c 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -50,19 +50,19 @@ class ClimateTraits { } void set_supported_modes(std::set modes) { supported_modes_ = std::move(modes); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_fan_only_mode(bool supports_fan_only_mode) { set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); } const std::set get_supported_modes() const { return supported_modes_; } @@ -72,23 +72,23 @@ class ClimateTraits { void set_supported_fan_modes(std::set modes) { supported_fan_modes_ = std::move(modes); } void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); } @@ -115,25 +115,25 @@ class ClimateTraits { bool supports_custom_preset(const std::string &custom_preset) const { return supported_custom_presets_.count(custom_preset); } - ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead", "v1.20") void set_supports_away(bool supports) { if (supports) { supported_presets_.insert(CLIMATE_PRESET_AWAY); supported_presets_.insert(CLIMATE_PRESET_HOME); } } - ESPDEPRECATED("This method is deprecated, use supports_preset() instead") + ESPDEPRECATED("This method is deprecated, use supports_preset() instead", "v1.20") bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); } void set_supported_swing_modes(std::set modes) { supported_swing_modes_ = std::move(modes); } void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") void set_supports_swing_mode_horizontal(bool supported) { set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); } diff --git a/esphome/components/color_temperature/__init__.py b/esphome/components/color_temperature/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/color_temperature/ct_light_output.h b/esphome/components/color_temperature/ct_light_output.h new file mode 100644 index 0000000000..4ff86c8b80 --- /dev/null +++ b/esphome/components/color_temperature/ct_light_output.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/components/light/light_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace color_temperature { + +class CTLightOutput : public light::LightOutput { + public: + void set_color_temperature(output::FloatOutput *color_temperature) { color_temperature_ = color_temperature; } + void set_brightness(output::FloatOutput *brightness) { brightness_ = brightness; } + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); + traits.set_min_mireds(this->cold_white_temperature_); + traits.set_max_mireds(this->warm_white_temperature_); + return traits; + } + void write_state(light::LightState *state) override { + float color_temperature, brightness; + state->current_values_as_ct(&color_temperature, &brightness); + this->color_temperature_->set_level(color_temperature); + this->brightness_->set_level(brightness); + } + + protected: + output::FloatOutput *color_temperature_; + output::FloatOutput *brightness_; + float cold_white_temperature_; + float warm_white_temperature_; +}; + +} // namespace color_temperature +} // namespace esphome diff --git a/esphome/components/color_temperature/light.py b/esphome/components/color_temperature/light.py new file mode 100644 index 0000000000..3e7a0e73ae --- /dev/null +++ b/esphome/components/color_temperature/light.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light, output +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_TEMPERATURE, + CONF_OUTPUT_ID, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, +) + +CODEOWNERS = ["@jesserockz"] + +color_temperature_ns = cg.esphome_ns.namespace("color_temperature") +CTLightOutput = color_temperature_ns.class_("CTLightOutput", light.LightOutput) + +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CTLightOutput), + cv.Required(CONF_COLOR_TEMPERATURE): cv.use_id(output.FloatOutput), + cv.Required(CONF_BRIGHTNESS): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + } + ), + light.validate_color_temperature_channels, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + color_temperature = await cg.get_variable(config[CONF_COLOR_TEMPERATURE]) + cg.add(var.set_color_temperature(color_temperature)) + + brightness = await cg.get_variable(config[CONF_BRIGHTNESS]) + cg.add(var.set_brightness(brightness)) + + cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 4a7266303d..6fd6ac81b0 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.automation import maybe_simple_id, Condition from esphome.components import mqtt from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, CONF_ID, CONF_INTERNAL, CONF_DEVICE_CLASS, @@ -63,7 +64,7 @@ CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) -COVER_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +COVER_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(Cover), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), @@ -75,6 +76,7 @@ COVER_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( async def setup_cover_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_DEVICE_CLASS in config: diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index efb1d1d426..82df881bfc 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - ICON_EMPTY, DEVICE_CLASS_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_VOLTAGE, @@ -80,13 +79,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOLTAGE_HPF, default=True): cv.boolean, cv.Optional(CONF_PULSE_ENERGY, default=10.0): validate_energy, cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, ), } ) diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 4ccb346efd..1c8efc4f72 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -28,17 +27,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(CSE7766Component), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index e44d46e7f4..049905d0a7 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_SENSOR, CONF_ID, DEVICE_CLASS_CURRENT, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, ) @@ -20,7 +19,10 @@ CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingCom CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 3351a98d24..2b7698ce5a 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -16,10 +16,7 @@ class CWWWLightOutput : public light::LightOutput { void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(false); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; @@ -34,8 +31,8 @@ class CWWWLightOutput : public light::LightOutput { protected: output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - float cold_white_temperature_; - float warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; }; diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index 1a027a86b4..fc204b2f3b 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output from esphome.const import ( + CONF_CONSTANT_BRIGHTNESS, CONF_OUTPUT_ID, CONF_COLD_WHITE, CONF_WARM_WHITE, @@ -12,19 +13,20 @@ from esphome.const import ( cwww_ns = cg.esphome_ns.namespace("cwww") CWWWLightOutput = cwww_ns.class_("CWWWLightOutput", light.LightOutput) -CONF_CONSTANT_BRIGHTNESS = "constant_brightness" - CONFIG_SCHEMA = cv.All( light.RGB_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, } ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), light.validate_color_temperature_channels, ) @@ -32,11 +34,19 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) + cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 7fc5e424f0..7e34546078 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -137,7 +137,7 @@ void DallasComponent::update() { } if (!res) { - ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str()); + ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str()); sensor->publish_state(NAN); this->status_set_warning(); return; diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 1c8db8fa2f..5e09701bae 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_INDEX, CONF_RESOLUTION, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, CONF_ID, @@ -18,7 +17,10 @@ DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sen CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.GenerateID(): cv.declare_id(DallasTemperatureSensor), diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py new file mode 100644 index 0000000000..b3ea47d869 --- /dev/null +++ b/esphome/components/demo/__init__.py @@ -0,0 +1,451 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + binary_sensor, + climate, + cover, + fan, + light, + number, + sensor, + switch, + text_sensor, +) +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_BINARY_SENSORS, + CONF_DEVICE_CLASS, + CONF_FORCE_UPDATE, + CONF_ICON, + CONF_ID, + CONF_INVERTED, + CONF_LAST_RESET_TYPE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_NAME, + CONF_OUTPUT_ID, + CONF_SENSORS, + CONF_STATE_CLASS, + CONF_STEP, + CONF_SWITCHES, + CONF_TEXT_SENSORS, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_TEMPERATURE, + ICON_BLUETOOTH, + ICON_BLUR, + ICON_EMPTY, + ICON_THERMOMETER, + LAST_RESET_TYPE_AUTO, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_PERCENT, + UNIT_WATT_HOURS, +) + +AUTO_LOAD = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "number", + "sensor", + "switch", + "text_sensor", +] + +demo_ns = cg.esphome_ns.namespace("demo") +DemoBinarySensor = demo_ns.class_( + "DemoBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent +) +DemoClimate = demo_ns.class_("DemoClimate", climate.Climate, cg.Component) +DemoClimateType = demo_ns.enum("DemoClimateType", is_class=True) +DemoCover = demo_ns.class_("DemoCover", cover.Cover, cg.Component) +DemoCoverType = demo_ns.enum("DemoCoverType", is_class=True) +DemoFan = demo_ns.class_("DemoFan", cg.Component) +DemoFanType = demo_ns.enum("DemoFanType", is_class=True) +DemoLight = demo_ns.class_("DemoLight", light.LightOutput, cg.Component) +DemoLightType = demo_ns.enum("DemoLightType", is_class=True) +DemoNumber = demo_ns.class_("DemoNumber", number.Number, cg.Component) +DemoNumberType = demo_ns.enum("DemoNumberType", is_class=True) +DemoSensor = demo_ns.class_("DemoSensor", sensor.Sensor, cg.PollingComponent) +DemoSwitch = demo_ns.class_("DemoSwitch", switch.Switch, cg.Component) +DemoTextSensor = demo_ns.class_( + "DemoTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + + +CLIMATE_TYPES = { + 1: DemoClimateType.TYPE_1, + 2: DemoClimateType.TYPE_2, + 3: DemoClimateType.TYPE_3, +} +COVER_TYPES = { + 1: DemoCoverType.TYPE_1, + 2: DemoCoverType.TYPE_2, + 3: DemoCoverType.TYPE_3, + 4: DemoCoverType.TYPE_4, +} +FAN_TYPES = { + 1: DemoFanType.TYPE_1, + 2: DemoFanType.TYPE_2, + 3: DemoFanType.TYPE_3, + 4: DemoFanType.TYPE_4, +} +LIGHT_TYPES = { + 1: DemoLightType.TYPE_1, + 2: DemoLightType.TYPE_2, + 3: DemoLightType.TYPE_3, + 4: DemoLightType.TYPE_4, + 5: DemoLightType.TYPE_5, + 6: DemoLightType.TYPE_6, + 7: DemoLightType.TYPE_7, +} +NUMBER_TYPES = { + 1: DemoNumberType.TYPE_1, + 2: DemoNumberType.TYPE_2, + 3: DemoNumberType.TYPE_3, +} + + +CONF_CLIMATES = "climates" +CONF_COVERS = "covers" +CONF_FANS = "fans" +CONF_LIGHTS = "lights" +CONF_NUMBERS = "numbers" + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional( + CONF_BINARY_SENSORS, + default=[ + { + CONF_NAME: "Demo Basement Floor Wet", + CONF_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + }, + { + CONF_NAME: "Demo Movement Backyard", + CONF_DEVICE_CLASS: DEVICE_CLASS_MOTION, + }, + ], + ): [ + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + cv.polling_component_schema("60s") + ).extend( + { + cv.GenerateID(): cv.declare_id(DemoBinarySensor), + } + ) + ], + cv.Optional( + CONF_CLIMATES, + default=[ + { + CONF_NAME: "Demo Heatpump", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo HVAC", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo Ecobee", + CONF_TYPE: 3, + }, + ], + ): [ + climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoClimate), + cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_COVERS, + default=[ + { + CONF_NAME: "Demo Kitchen Window", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Garage Door", + CONF_TYPE: 2, + CONF_DEVICE_CLASS: "garage", + }, + { + CONF_NAME: "Demo Living Room Window", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo Hall Window", + CONF_TYPE: 4, + CONF_DEVICE_CLASS: "window", + }, + ], + ): [ + cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoCover), + cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_FANS, + default=[ + { + CONF_NAME: "Demo Living Room Fan", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Ceiling Fan", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo Percentage Limited Fan", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo Percentage Full Fan", + CONF_TYPE: 4, + }, + ], + ): [ + fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan), + cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_LIGHTS, + default=[ + { + CONF_NAME: "Demo Binary Light", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Brightness Light", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo RGB Light", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo RGBW Light", + CONF_TYPE: 4, + }, + { + CONF_NAME: "Demo RGBWW Light", + CONF_TYPE: 5, + }, + { + CONF_NAME: "Demo CWWW Light", + CONF_TYPE: 6, + }, + { + CONF_NAME: "Demo RGBW interlock Light", + CONF_TYPE: 7, + }, + ], + ): [ + light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight), + cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_NUMBERS, + default=[ + { + CONF_NAME: "Demo Number 0-100", + CONF_TYPE: 1, + CONF_MIN_VALUE: 0, + CONF_MAX_VALUE: 100, + CONF_STEP: 1, + }, + { + CONF_NAME: "Demo Number -50-50", + CONF_TYPE: 2, + CONF_MIN_VALUE: -50, + CONF_MAX_VALUE: 50, + CONF_STEP: 5, + }, + { + CONF_NAME: "Demo Number 40-60", + CONF_TYPE: 3, + CONF_MIN_VALUE: 40, + CONF_MAX_VALUE: 60, + CONF_STEP: 0.2, + }, + ], + ): [ + number.NUMBER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoNumber), + cv.Required(CONF_TYPE): cv.enum(NUMBER_TYPES, int=True), + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.float_, + } + ) + ], + cv.Optional( + CONF_SENSORS, + default=[ + { + CONF_NAME: "Demo Plain Sensor", + }, + { + CONF_NAME: "Demo Temperature Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_ICON: ICON_THERMOMETER, + CONF_ACCURACY_DECIMALS: 1, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + { + CONF_NAME: "Demo Temperature Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_ICON: ICON_THERMOMETER, + CONF_ACCURACY_DECIMALS: 1, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + { + CONF_NAME: "Demo Force Update Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_PERCENT, + CONF_ACCURACY_DECIMALS: 0, + CONF_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_FORCE_UPDATE: True, + }, + { + CONF_NAME: "Demo Energy Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, + CONF_ACCURACY_DECIMALS: 0, + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO, + }, + ], + ): [ + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0) + .extend(cv.polling_component_schema("60s")) + .extend( + { + cv.GenerateID(): cv.declare_id(DemoSensor), + } + ) + ], + cv.Optional( + CONF_SWITCHES, + default=[ + { + CONF_NAME: "Demo Switch 1", + }, + { + CONF_NAME: "Demo Switch 2", + CONF_INVERTED: True, + CONF_ICON: ICON_BLUETOOTH, + }, + ], + ): [ + switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoSwitch), + } + ) + ], + cv.Optional( + CONF_TEXT_SENSORS, + default=[ + { + CONF_NAME: "Demo Text Sensor 1", + }, + { + CONF_NAME: "Demo Text Sensor 2", + CONF_ICON: ICON_BLUR, + }, + ], + ): [ + text_sensor.TEXT_SENSOR_SCHEMA.extend( + cv.polling_component_schema("60s") + ).extend( + { + cv.GenerateID(): cv.declare_id(DemoTextSensor), + } + ) + ], + } +) + + +async def to_code(config): + for conf in config[CONF_BINARY_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await binary_sensor.register_binary_sensor(var, conf) + + for conf in config[CONF_CLIMATES]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await climate.register_climate(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_COVERS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await cover.register_cover(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_FANS]: + var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + await cg.register_component(var, conf) + fan_ = await fan.create_fan_state(conf) + cg.add(var.set_fan(fan_)) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_LIGHTS]: + var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + await cg.register_component(var, conf) + await light.register_light(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_NUMBERS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await number.register_number( + var, + conf, + min_value=conf[CONF_MIN_VALUE], + max_value=conf[CONF_MAX_VALUE], + step=conf[CONF_STEP], + ) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await sensor.register_sensor(var, conf) + + for conf in config[CONF_SWITCHES]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await switch.register_switch(var, conf) + + for conf in config[CONF_TEXT_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await text_sensor.register_text_sensor(var, conf) diff --git a/esphome/components/demo/demo_binary_sensor.h b/esphome/components/demo/demo_binary_sensor.h new file mode 100644 index 0000000000..4dfd038761 --- /dev/null +++ b/esphome/components/demo/demo_binary_sensor.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace demo { + +class DemoBinarySensor : public binary_sensor::BinarySensor, public PollingComponent { + public: + void setup() override { this->publish_initial_state(false); } + void update() override { + bool new_state = last_state_ = !last_state_; + this->publish_state(new_state); + } + + protected: + bool last_state_ = false; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h new file mode 100644 index 0000000000..0cf48dd4ee --- /dev/null +++ b/esphome/components/demo/demo_climate.h @@ -0,0 +1,157 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace demo { + +enum class DemoClimateType { + TYPE_1, + TYPE_2, + TYPE_3, +}; + +class DemoClimate : public climate::Climate, public Component { + public: + void set_type(DemoClimateType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoClimateType::TYPE_1: + this->current_temperature = 20.0; + this->target_temperature = 21.0; + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + break; + case DemoClimateType::TYPE_2: + this->target_temperature = 21.5; + this->mode = climate::CLIMATE_MODE_AUTO; + this->action = climate::CLIMATE_ACTION_COOLING; + this->fan_mode = climate::CLIMATE_FAN_HIGH; + this->custom_preset = {"My Preset"}; + break; + case DemoClimateType::TYPE_3: + this->current_temperature = 21.5; + this->target_temperature_low = 21.0; + this->target_temperature_high = 22.5; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + this->custom_fan_mode = {"Auto Low"}; + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + this->preset = climate::CLIMATE_PRESET_AWAY; + break; + } + this->publish_state(); + } + + protected: + void control(const climate::ClimateCall &call) override { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + } + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + } + if (call.get_target_temperature_low().has_value()) { + this->target_temperature_low = *call.get_target_temperature_low(); + } + if (call.get_target_temperature_high().has_value()) { + this->target_temperature_high = *call.get_target_temperature_high(); + } + if (call.get_fan_mode().has_value()) { + this->fan_mode = *call.get_fan_mode(); + this->custom_fan_mode.reset(); + } + if (call.get_swing_mode().has_value()) { + this->swing_mode = *call.get_swing_mode(); + } + if (call.get_custom_fan_mode().has_value()) { + this->custom_fan_mode = *call.get_custom_fan_mode(); + this->fan_mode.reset(); + } + if (call.get_preset().has_value()) { + this->preset = *call.get_preset(); + this->custom_preset.reset(); + } + if (call.get_custom_preset().has_value()) { + this->custom_preset = *call.get_custom_preset(); + this->preset.reset(); + } + this->publish_state(); + } + climate::ClimateTraits traits() override { + climate::ClimateTraits traits{}; + switch (type_) { + case DemoClimateType::TYPE_1: + traits.set_supports_current_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + }); + traits.set_supports_action(true); + traits.set_visual_temperature_step(0.5); + break; + case DemoClimateType::TYPE_2: + traits.set_supports_current_temperature(false); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_AUTO, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_FAN_ONLY, + }); + traits.set_supports_action(true); + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_ON, + climate::CLIMATE_FAN_OFF, + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_MIDDLE, + climate::CLIMATE_FAN_FOCUS, + climate::CLIMATE_FAN_DIFFUSE, + }); + traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, + }); + traits.set_supported_custom_presets({"My Preset"}); + break; + case DemoClimateType::TYPE_3: + traits.set_supports_current_temperature(true); + traits.set_supports_two_point_target_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_HEAT_COOL, + }); + traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_HORIZONTAL, + }); + traits.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_COMFORT, + climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_SLEEP, + climate::CLIMATE_PRESET_ACTIVITY, + }); + break; + } + return traits; + } + + DemoClimateType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h new file mode 100644 index 0000000000..ab039736fb --- /dev/null +++ b/esphome/components/demo/demo_cover.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace demo { + +enum class DemoCoverType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4, +}; + +class DemoCover : public cover::Cover, public Component { + public: + void set_type(DemoCoverType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoCoverType::TYPE_1: + this->position = cover::COVER_OPEN; + break; + case DemoCoverType::TYPE_2: + this->position = 0.7; + break; + case DemoCoverType::TYPE_3: + this->position = 0.1; + this->tilt = 0.8; + break; + case DemoCoverType::TYPE_4: + this->position = cover::COVER_CLOSED; + this->tilt = 1.0; + break; + } + this->publish_state(); + } + + protected: + void control(const cover::CoverCall &call) override { + if (call.get_position().has_value()) { + float target = *call.get_position(); + this->current_operation = + target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; + + this->set_timeout("move", 2000, [this, target]() { + this->current_operation = cover::COVER_OPERATION_IDLE; + this->position = target; + this->publish_state(); + }); + } + if (call.get_tilt().has_value()) { + this->tilt = *call.get_tilt(); + } + if (call.get_stop()) { + this->cancel_timeout("move"); + } + + this->publish_state(); + } + cover::CoverTraits get_traits() override { + cover::CoverTraits traits{}; + switch (type_) { + case DemoCoverType::TYPE_1: + traits.set_is_assumed_state(true); + break; + case DemoCoverType::TYPE_2: + traits.set_supports_position(true); + break; + case DemoCoverType::TYPE_3: + traits.set_supports_position(true); + traits.set_supports_tilt(true); + break; + case DemoCoverType::TYPE_4: + traits.set_is_assumed_state(true); + traits.set_supports_tilt(true); + break; + } + return traits; + } + + DemoCoverType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h new file mode 100644 index 0000000000..e926f68edb --- /dev/null +++ b/esphome/components/demo/demo_fan.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace demo { + +enum class DemoFanType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4, +}; + +class DemoFan : public Component { + public: + void set_type(DemoFanType type) { type_ = type; } + void set_fan(fan::FanState *fan) { fan_ = fan; } + void setup() override { + fan::FanTraits traits{}; + + // oscillation + // speed + // direction + // speed_count + switch (type_) { + case DemoFanType::TYPE_1: + break; + case DemoFanType::TYPE_2: + traits.set_oscillation(true); + break; + case DemoFanType::TYPE_3: + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(5); + break; + case DemoFanType::TYPE_4: + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(100); + traits.set_oscillation(true); + break; + } + + this->fan_->set_traits(traits); + } + + fan::FanState *fan_; + DemoFanType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_light.h b/esphome/components/demo/demo_light.h new file mode 100644 index 0000000000..2007e9ff50 --- /dev/null +++ b/esphome/components/demo/demo_light.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace demo { + +enum class DemoLightType { + // binary + TYPE_1, + // brightness + TYPE_2, + // RGB + TYPE_3, + // RGBW + TYPE_4, + // RGBWW + TYPE_5, + // CWWW + TYPE_6, + // RGBW + color_interlock + TYPE_7, +}; + +class DemoLight : public light::LightOutput, public Component { + public: + void set_type(DemoLightType type) { type_ = type; } + light::LightTraits get_traits() override { + light::LightTraits traits{}; + switch (type_) { + case DemoLightType::TYPE_1: + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); + break; + case DemoLightType::TYPE_2: + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + break; + case DemoLightType::TYPE_3: + traits.set_supported_color_modes({light::ColorMode::RGB}); + break; + case DemoLightType::TYPE_4: + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + break; + case DemoLightType::TYPE_5: + traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + break; + case DemoLightType::TYPE_6: + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + break; + case DemoLightType::TYPE_7: + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + break; + } + return traits; + } + void write_state(light::LightState *state) override { + // do nothing + } + + DemoLightType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_number.h b/esphome/components/demo/demo_number.h new file mode 100644 index 0000000000..2ce3a269bc --- /dev/null +++ b/esphome/components/demo/demo_number.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace demo { + +enum class DemoNumberType { + TYPE_1, + TYPE_2, + TYPE_3, +}; + +class DemoNumber : public number::Number, public Component { + public: + void set_type(DemoNumberType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoNumberType::TYPE_1: + this->publish_state(50); + break; + case DemoNumberType::TYPE_2: + this->publish_state(-10); + break; + case DemoNumberType::TYPE_3: + this->publish_state(42); + break; + } + } + + protected: + void control(float value) override { this->publish_state(value); } + + DemoNumberType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h new file mode 100644 index 0000000000..117468793b --- /dev/null +++ b/esphome/components/demo/demo_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace demo { + +class DemoSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override { + float val = random_float(); + bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO; + if (is_auto) { + float base = isnan(this->state) ? 0.0f : this->state; + this->publish_state(base + val * 10); + } else { + if (val < 0.1) + this->publish_state(NAN); + else + this->publish_state(val * 100); + } + } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_switch.h b/esphome/components/demo/demo_switch.h new file mode 100644 index 0000000000..9c291318ca --- /dev/null +++ b/esphome/components/demo/demo_switch.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace demo { + +class DemoSwitch : public switch_::Switch, public Component { + public: + void setup() override { + bool initial = random_float() < 0.5; + this->publish_state(initial); + } + + protected: + void write_state(bool state) override { this->publish_state(state); } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_text_sensor.h b/esphome/components/demo/demo_text_sensor.h new file mode 100644 index 0000000000..b4152fc248 --- /dev/null +++ b/esphome/components/demo/demo_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace demo { + +class DemoTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void update() override { + float val = random_float(); + if (val < 0.33) { + this->publish_state("foo"); + } else if (val < 0.66) { + this->publish_state("bar"); + } else { + this->publish_state("foobar"); + } + } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 5cd49c311d..ae47cb33f1 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -116,7 +116,7 @@ DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) template class PlayFileAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, file) - TEMPLATABLE_VALUE(boolean, loop) + TEMPLATABLE_VALUE(bool, loop) void play(Ts... x) override { auto file = this->file_.value(x...); @@ -133,7 +133,7 @@ template class PlayFolderAction : public Action, public P public: TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) - TEMPLATABLE_VALUE(boolean, loop) + TEMPLATABLE_VALUE(bool, loop) void play(Ts... x) override { auto folder = this->folder_.value(x...); diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 4a5c418e0a..a7f5747d68 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -203,7 +203,7 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, const uint16_t raw_humidity = uint16_t(data[0]) * 10 + data[1]; *humidity = raw_humidity / 10.0f; } else { - // For compatibily with DHT11 models which might only use 2 bytes checksums, only use the data from these two + // For compatibility with DHT11 models which might only use 2 bytes checksums, only use the data from these two // bytes *temperature = data[2]; *humidity = data[0]; diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index c33ddd2286..1334f0270c 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -36,14 +35,16 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(DHT), cv.Required(CONF_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MODEL, default="auto detect"): cv.enum( DHT_MODELS, upper=True, space="_" diff --git a/esphome/components/dht12/sensor.py b/esphome/components/dht12/sensor.py index 14c01f5d34..ae2173ef22 100644 --- a/esphome/components/dht12/sensor.py +++ b/esphome/components/dht12/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(DHT12Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 08050b2078..29f86dbd5f 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -14,7 +14,7 @@ const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); void DisplayBuffer::init_internal_(uint32_t buffer_length) { - this->buffer_ = new uint8_t[buffer_length]; + this->buffer_ = new (std::nothrow) uint8_t[buffer_length]; if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); return; @@ -461,7 +461,7 @@ bool Image::get_pixel(int x, int y) const { } Color Image::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_) * 3; const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | (pgm_read_byte(this->data_start_ + pos + 1) << 8) | @@ -470,7 +470,7 @@ Color Image::get_color_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_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_); const uint8_t gray = pgm_read_byte(this->data_start_ + pos); return Color(gray | gray << 8 | gray << 16 | gray << 24); @@ -493,10 +493,10 @@ bool Animation::get_pixel(int x, int y) const { } Color Animation::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return 0; + return Color::BLACK; const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_ + frame_index) * 3; const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | (pgm_read_byte(this->data_start_ + pos + 1) << 8) | @@ -505,10 +505,10 @@ Color Animation::get_color_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_) - return 0; + return Color::BLACK; const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) - return 0; + return Color::BLACK; const uint32_t pos = (x + y * this->width_ + frame_index); const uint8_t gray = pgm_read_byte(this->data_start_ + pos); return Color(gray | gray << 8 | gray << 16 | gray << 24); diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py new file mode 100644 index 0000000000..df11a14ee8 --- /dev/null +++ b/esphome/components/dsmr/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import ( + CONF_ID, + CONF_UART_ID, +) + +CODEOWNERS = ["@glmnet", "@zuidwijk"] + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_DSMR_ID = "dsmr_id" +CONF_DECRYPTION_KEY = "decryption_key" + +# Hack to prevent compile error due to ambiguity with lib namespace +dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr") +Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) + + +def _validate_key(value): + value = cv.string_strict(value) + parts = [value[i : i + 2] for i in range(0, len(value), 2)] + if len(parts) != 16: + raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers") + parts_int = [] + if any(len(part) != 2 for part in parts): + raise cv.Invalid("Decryption key must be format XX") + for part in parts: + try: + parts_int.append(int(part, 16)) + except ValueError: + # pylint: disable=raise-missing-from + raise cv.Invalid("Decryption key must be hex values from 00 to FF") + + return "".join(f"{part:02X}" for part in parts_int) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Dsmr), + cv.Optional(CONF_DECRYPTION_KEY): _validate_key, + } +).extend(uart.UART_DEVICE_SCHEMA) + + +async def to_code(config): + uart_component = await cg.get_variable(config[CONF_UART_ID]) + var = cg.new_Pvariable(config[CONF_ID], uart_component) + if CONF_DECRYPTION_KEY in config: + cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) + await cg.register_component(var, config) + + # DSMR Parser + cg.add_library("glmnet/Dsmr", "0.3") + + # Crypto + cg.add_library("rweather/Crypto", "0.2.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp new file mode 100644 index 0000000000..9bce7a382f --- /dev/null +++ b/esphome/components/dsmr/dsmr.cpp @@ -0,0 +1,182 @@ +#include "dsmr.h" +#include "esphome/core/log.h" + +#include +#include +#include + +namespace esphome { +namespace dsmr { + +static const char *const TAG = "dsmr"; + +void Dsmr::loop() { + if (this->decryption_key_.empty()) + this->receive_telegram_(); + else + this->receive_encrypted_(); +} + +void Dsmr::receive_telegram_() { + while (available()) { + const char c = read(); + + if (c == '/') { // header: forward slash + ESP_LOGV(TAG, "Header found"); + header_found_ = true; + footer_found_ = false; + telegram_len_ = 0; + } + + if (!header_found_) + continue; + if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { // Buffer overflow + header_found_ = false; + footer_found_ = false; + ESP_LOGE(TAG, "Error: Message larger than buffer"); + return; + } + + telegram_[telegram_len_] = c; + telegram_len_++; + if (c == '!') { // footer: exclamation mark + ESP_LOGV(TAG, "Footer found"); + footer_found_ = true; + } else { + if (footer_found_ && c == 10) { // last \n after footer + header_found_ = false; + // Parse message + if (parse_telegram()) + return; + } + } + } +} + +void Dsmr::receive_encrypted_() { + // Encrypted buffer + uint8_t buffer[MAX_TELEGRAM_LENGTH]; + size_t buffer_length = 0; + + size_t packet_size = 0; + while (available()) { + const char c = read(); + + if (!header_found_) { + if ((uint8_t) c == 0xdb) { + ESP_LOGV(TAG, "Start byte 0xDB found"); + header_found_ = true; + } + } + + // Sanity check + if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { + if (buffer_length == 0) { + ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); + } else { + ESP_LOGW(TAG, "Unexpected data"); + } + this->status_momentary_warning("unexpected_data"); + this->flush(); + while (available()) + read(); + return; + } + + buffer[buffer_length++] = c; + + if (packet_size == 0 && buffer_length > 20) { + // Complete header + a few bytes of data + packet_size = buffer[11] << 8 | buffer[12]; + } + if (buffer_length == packet_size + 13 && packet_size > 0) { + ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); + + GCM *gcmaes128{new GCM()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + buffer[i] = buffer[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&buffer[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &buffer[18], + // cipher size + buffer_length - 17); + delete gcmaes128; + + telegram_len_ = strnlen(this->telegram_, sizeof(this->telegram_)); + ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); + ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); + + parse_telegram(); + telegram_len_ = 0; + return; + } + + if (!available()) { + // baud rate is 115200 for encrypted data, this means a few byte should arrive every time + // program runs faster than buffer loading then available() might return false in the middle + delay(4); // Wait for data + } + } + if (buffer_length > 0) + ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); +} + +bool Dsmr::parse_telegram() { + MyData data; + ESP_LOGV(TAG, "Trying to parse"); + ::dsmr::ParseResult res = + ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, + false); // Parse telegram according to data definition. Ignore unknown values. + if (res.err) { + // Parsing error, show it + auto err_str = res.fullError(telegram_, telegram_ + telegram_len_); + ESP_LOGE(TAG, "%s", err_str.c_str()); + return false; + } else { + this->status_clear_warning(); + publish_sensors(data); + return true; + } +} + +void Dsmr::dump_config() { + ESP_LOGCONFIG(TAG, "dsmr:"); + +#define DSMR_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s_##s##_); + DSMR_SENSOR_LIST(DSMR_LOG_SENSOR, ) + +#define DSMR_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s_##s##_); + DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) +} + +void Dsmr::set_decryption_key(const std::string &decryption_key) { + if (decryption_key.length() == 0) { + ESP_LOGI(TAG, "Disabling decryption"); + this->decryption_key_.clear(); + return; + } + + if (decryption_key.length() != 32) { + ESP_LOGE(TAG, "Error, decryption key must be 32 character long."); + return; + } + this->decryption_key_.clear(); + + ESP_LOGI(TAG, "Decryption key is set."); + // Verbose level prints decryption key + ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); + + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(decryption_key.c_str()[i * 2]), 2); + decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); + } +} + +} // namespace dsmr +} // namespace esphome diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h new file mode 100644 index 0000000000..984f2596db --- /dev/null +++ b/esphome/components/dsmr/dsmr.h @@ -0,0 +1,104 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" + +// don't include because it puts everything in global namespace +#include +#include + +namespace esphome { +namespace dsmr { + +static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; +static constexpr uint32_t POLL_TIMEOUT = 1000; + +using namespace ::dsmr::fields; + +// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines + +#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST) +// Neither set, set it to a dummy value to not break build +#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification) +#endif + +#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST) +#define DSMR_BOTH , +#else +#define DSMR_BOTH +#endif + +#ifndef DSMR_SENSOR_LIST +#define DSMR_SENSOR_LIST(F, SEP) +#endif + +#ifndef DSMR_TEXT_SENSOR_LIST +#define DSMR_TEXT_SENSOR_LIST(F, SEP) +#endif + +#define DSMR_DATA_SENSOR(s) s +#define DSMR_COMMA , + +using MyData = ::dsmr::ParsedData; + +class Dsmr : public Component, public uart::UARTDevice { + public: + Dsmr(uart::UARTComponent *uart) : uart::UARTDevice(uart) {} + + void loop() override; + + bool parse_telegram(); + + void publish_sensors(MyData &data) { +#define DSMR_PUBLISH_SENSOR(s) \ + if (data.s##_present && this->s_##s##_ != nullptr) \ + s_##s##_->publish_state(data.s); + DSMR_SENSOR_LIST(DSMR_PUBLISH_SENSOR, ) + +#define DSMR_PUBLISH_TEXT_SENSOR(s) \ + if (data.s##_present && this->s_##s##_ != nullptr) \ + s_##s##_->publish_state(data.s.c_str()); + DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, ) + }; + + void dump_config() override; + + void set_decryption_key(const std::string &decryption_key); + +// Sensor setters +#define DSMR_SET_SENSOR(s) \ + void set_##s(sensor::Sensor *sensor) { s_##s##_ = sensor; } + DSMR_SENSOR_LIST(DSMR_SET_SENSOR, ) + +#define DSMR_SET_TEXT_SENSOR(s) \ + void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; } + DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) + + protected: + void receive_telegram_(); + void receive_encrypted_(); + + // Telegram buffer + char telegram_[MAX_TELEGRAM_LENGTH]; + int telegram_len_{0}; + + // Serial parser + bool header_found_{false}; + bool footer_found_{false}; + +// Sensor member pointers +#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; + DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) + +#define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr}; + DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, ) + + std::vector decryption_key_{}; +}; +} // namespace dsmr +} // namespace esphome diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py new file mode 100644 index 0000000000..05c568e21a --- /dev/null +++ b/esphome/components/dsmr/sensor.py @@ -0,0 +1,210 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + LAST_RESET_TYPE_NEVER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_AMPERE, + UNIT_EMPTY, + UNIT_VOLT, + UNIT_WATT, +) +from . import Dsmr, CONF_DSMR_ID + +AUTO_LOAD = ["dsmr"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), + cv.Optional("energy_delivered_lux"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_returned_lux"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("total_imported_energy"): sensor.sensor_schema( + "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + ), + cv.Optional("total_exported_energy"): sensor.sensor_schema( + "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + ), + cv.Optional("power_delivered"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered"): sensor.sensor_schema( + "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + ), + cv.Optional("reactive_power_returned"): sensor.sensor_schema( + "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + ), + cv.Optional("electricity_threshold"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_switch_position"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_failures"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_long_failures"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l1"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l2"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + ), + cv.Optional("electricity_sags_l3"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l1"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + ), + cv.Optional("electricity_swells_l2"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l3"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("current_l1"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("current_l2"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("current_l3"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("voltage_l1"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("voltage_l2"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("voltage_l3"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("gas_delivered"): sensor.sensor_schema( + "m³", + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("gas_delivered_be"): sensor.sensor_schema( + "m³", + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DSMR_ID]) + + sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf.get("id") + if id and id.type == sensor.Sensor: + s = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}")(s)) + sensors.append(f"F({key})") + + cg.add_define("DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py new file mode 100644 index 0000000000..821b07dc6b --- /dev/null +++ b/esphome/components/dsmr/text_sensor.py @@ -0,0 +1,94 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_ID, +) +from . import Dsmr, CONF_DSMR_ID + +AUTO_LOAD = ["dsmr"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), + cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("sub_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DSMR_ID]) + + text_sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf.get("id") + if id and id.type == text_sensor.TextSensor: + var = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(var, conf) + cg.add(getattr(hub, f"set_{key}")(var)) + text_sensors.append(f"F({key})") + + cg.add_define( + "DSMR_TEXT_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(text_sensors)) + ) diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py index 39f6ebc88f..3537cb0973 100644 --- a/esphome/components/duty_cycle/sensor.py +++ b/esphome/components/duty_cycle/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_PIN, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_PERCENT, @@ -18,7 +17,10 @@ DutyCycleSensor = duty_cycle_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PERCENT, ICON_PERCENT, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index fb72e5b470..bd749683c5 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -50,7 +50,7 @@ void E131Component::loop() { } if (!packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); + ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); continue; } diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index f215fb48a3..92270124dd 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -7,6 +7,8 @@ namespace esphome { namespace esp32_ble { +static const char *const TAG = "esp32_ble"; + BLEAdvertising::BLEAdvertising() { this->advertising_data_.set_scan_rsp = false; this->advertising_data_.include_name = true; diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index cb0b99c62b..0de938d9a8 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -5,6 +5,8 @@ namespace esphome { namespace esp32_ble { +static const char *const TAG = "esp32_ble"; + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 3c87a90f22..cd123d5469 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -13,7 +13,7 @@ /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal wth various locking strategies, all incoming GAP and GATT + * than trying to deal with various locking strategies, all incoming GAP and GATT * events will simply be placed on a semaphore guarded queue. The next time the * component runs loop(), these events are popped off the queue and handed at * this safer time. diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 2a3403f88d..b3db651655 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -372,7 +372,7 @@ std::string ESPBTUUID::to_string() { for (int8_t i = 15; i >= 0; i--) { sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); bpos += 2; - if (i == 3 || i == 5 || i == 7 || i == 9) + if (i == 6 || i == 8 || i == 10 || i == 12) sprintf(bpos++, "-"); } sbuf[47] = '\0'; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h index 17adb98034..f0f6ab9f17 100644 --- a/esphome/components/esp32_ble_tracker/queue.h +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -12,7 +12,7 @@ /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal wth various locking strategies, all incoming GAP and GATT + * than trying to deal with various locking strategies, all incoming GAP and GATT * events will simply be placed on a semaphore guarded queue. The next time the * component runs loop(), these events are popped off the queue and handed at * this safer time. diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index abbb4b1b7e..e3c7383953 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, CONF_FREQUENCY, CONF_ID, CONF_NAME, @@ -66,6 +67,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32Camera), cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_DISABLED_BY_DEFAULT, default=False): cv.boolean, cv.Required(CONF_DATA_PINS): cv.All([pins.input_pin], cv.Length(min=8, max=8)), cv.Required(CONF_VSYNC_PIN): pins.input_pin, cv.Required(CONF_HREF_PIN): pins.input_pin, @@ -124,6 +126,7 @@ SETTERS = { async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) await cg.register_component(var, config) for key, setter in SETTERS.items(): diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py index b800b3436a..4ba79de714 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ESP_PLATFORM_ESP32, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA, @@ -19,7 +18,10 @@ ESP32HallSensor = esp32_hall_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 52c184eef6..b22f8c97d1 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -9,7 +9,7 @@ #include #include -/// Macro for IDF version comparision +/// Macro for IDF version comparison #ifndef ESP_IDF_VERSION_VAL #define ESP_IDF_VERSION_VAL(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) #endif @@ -102,8 +102,6 @@ void EthernetComponent::loop() { this->dump_connect_params_(); this->status_clear_warning(); - - network_tick_mdns(); } else if (now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); this->start_connect_(); @@ -120,6 +118,8 @@ void EthernetComponent::loop() { } break; } + + network_tick_mdns(); } void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, "Ethernet:"); diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index d1f43467ed..9db2e9ed12 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import mqtt from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, CONF_ID, CONF_INTERNAL, CONF_MQTT_ID, @@ -34,7 +35,7 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) -FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(FanState), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), @@ -66,6 +67,7 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( async def setup_fan_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 59d143dbef..ac6acc95a5 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -208,8 +208,7 @@ class FastLEDLightOutput : public light::AddressableLight { // (In most use cases you won't need these) light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void setup() override; diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index cfc62e930b..d437d01dcf 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -31,6 +31,7 @@ CHIPSETS = [ "GW6205_400", "LPD1886", "LPD1886_8BIT", + "SM16703", ] diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index f8a44eb0da..f359a10348 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -8,15 +8,12 @@ from esphome.const import ( CONF_LAST_FINGER_ID, CONF_SECURITY_LEVEL, CONF_STATUS, - DEVICE_CLASS_EMPTY, ICON_ACCOUNT, ICON_ACCOUNT_CHECK, ICON_DATABASE, - ICON_EMPTY, ICON_FINGERPRINT, ICON_SECURITY, STATE_CLASS_NONE, - UNIT_EMPTY, ) from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent @@ -26,22 +23,33 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( - UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_FINGERPRINT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_STATUS): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_CAPACITY): sensor.sensor_schema( - UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_DATABASE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( - UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_SECURITY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_ACCOUNT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_ACCOUNT_CHECK, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 7a26cd7b6b..8dc7a3e484 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -62,7 +62,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { /// Transmit via IR power off command. void transmit_off_(); - /// Parse incomming message + /// Parse incoming message bool on_receive(remote_base::RemoteReceiveData data) override; /// Transmit message as IR pulses diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 2867aa7325..0d5c3f3f3d 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -15,9 +15,6 @@ from esphome.const import ( UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, - UNIT_EMPTY, - ICON_EMPTY, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["uart"] @@ -36,26 +33,33 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(GPS), cv.Optional(CONF_LATITUDE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( - UNIT_KILOMETER_PER_HOUR, - ICON_EMPTY, - 6, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=2, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( - UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_METER, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 1926d4d68a..1d685b9b2e 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -9,12 +9,10 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -63,24 +61,47 @@ HavellsSolar = havells_solar_ns.class_( ) PHASE_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), } PV_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_INSULATION_OF_P_TO_GROUND: sensor.sensor_schema( - UNIT_KOHM, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } @@ -101,107 +122,86 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PV1): PV_SCHEMA, cv.Optional(CONF_PV2): PV_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( - UNIT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_HOURS, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( - UNIT_MINUTE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_MINUTE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( - UNIT_DEGREES, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INVERTER_INNER_TEMP): sensor.sensor_schema( - UNIT_DEGREES, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INVERTER_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INSULATION_OF_PV_N_TO_GROUND): sensor.sensor_schema( - UNIT_KOHM, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_GFCI_VALUE): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_R): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_S): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_T): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/hbridge_light_output.h index 03a5b3a88c..2f4f87134c 100644 --- a/esphome/components/hbridge/hbridge_light_output.h +++ b/esphome/components/hbridge/hbridge_light_output.h @@ -18,10 +18,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); // Dimming - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(true); // hbridge color - traits.set_supports_color_temperature(false); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); return traits; } @@ -31,11 +28,11 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { // This method runs around 60 times per second // We cannot do the PWM ourselves so we are reliant on the hardware PWM if (!this->forward_direction_) { // First LED Direction - this->pinb_pin_->set_level(this->duty_off_); this->pina_pin_->set_level(this->pina_duty_); + this->pinb_pin_->set_level(0); this->forward_direction_ = true; } else { // Second LED Direction - this->pina_pin_->set_level(this->duty_off_); + this->pina_pin_->set_level(0); this->pinb_pin_->set_level(this->pinb_duty_); this->forward_direction_ = false; } @@ -44,23 +41,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { float get_setup_priority() const override { return setup_priority::HARDWARE; } void write_state(light::LightState *state) override { - float bright; - state->current_values_as_brightness(&bright); - - state->set_gamma_correct(0); - float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); - - if ((white / bright) > 0.55) { - this->pina_duty_ = (bright * (1 - (white / bright))); - this->pinb_duty_ = bright; - } else if (white < 0.45) { - this->pina_duty_ = bright; - this->pinb_duty_ = white; - } else { - this->pina_duty_ = bright; - this->pinb_duty_ = bright; - } + state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); } protected: @@ -68,7 +49,6 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { output::FloatOutput *pinb_pin_; float pina_duty_ = 0; float pinb_duty_ = 0; - float duty_off_ = 0; bool forward_direction_ = false; }; diff --git a/esphome/components/hdc1080/sensor.py b/esphome/components/hdc1080/sensor.py index 26ec3ad0a9..39727f7159 100644 --- a/esphome/components/hdc1080/sensor.py +++ b/esphome/components/hdc1080/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(HDC1080Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hitachi_ac424/__init__.py b/esphome/components/hitachi_ac424/__init__.py new file mode 100644 index 0000000000..10f2c27fe8 --- /dev/null +++ b/esphome/components/hitachi_ac424/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@sourabhjaiswal"] diff --git a/esphome/components/hitachi_ac424/climate.py b/esphome/components/hitachi_ac424/climate.py new file mode 100644 index 0000000000..33532230df --- /dev/null +++ b/esphome/components/hitachi_ac424/climate.py @@ -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"] + +hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") +HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HitachiClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp new file mode 100644 index 0000000000..10b83cbd58 --- /dev/null +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -0,0 +1,368 @@ +#include "hitachi_ac424.h" + +namespace esphome { +namespace hitachi_ac424 { + +static const char *const TAG = "climate.hitachi_ac424"; + +void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, const uint8_t data) { + if (offset >= 8 || !nbits) + return; // Short circuit as it won't change. + // Calculate the mask for the supplied value. + uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); + // Calculate the mask & clear the space for the data. + // Clear the destination bits. + *dst &= ~(uint8_t)(mask << offset); + // Merge in the data. + *dst |= ((data & mask) << offset); +} + +void set_bit(uint8_t *const data, const uint8_t position, const bool on) { + uint8_t mask = 1 << position; + if (on) + *data |= mask; + else + *data &= ~mask; +} + +uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) { + for (uint16_t i = 1; i < length; i += 2) { + // Code done this way to avoid a compiler warning bug. + uint8_t inv = ~*(ptr + i - 1); + *(ptr + i) = inv; + } + return ptr; +} + +bool HitachiClimate::get_power_() { return remote_state_[HITACHI_AC424_POWER_BYTE] == HITACHI_AC424_POWER_ON; } + +void HitachiClimate::set_power_(bool on) { + set_button_(HITACHI_AC424_BUTTON_POWER); + remote_state_[HITACHI_AC424_POWER_BYTE] = on ? HITACHI_AC424_POWER_ON : HITACHI_AC424_POWER_OFF; +} + +uint8_t HitachiClimate::get_mode_() { return remote_state_[HITACHI_AC424_MODE_BYTE] & 0xF; } + +void HitachiClimate::set_mode_(uint8_t mode) { + uint8_t new_mode = mode; + switch (mode) { + // Fan mode sets a special temp. + case HITACHI_AC424_MODE_FAN: + set_temp_(HITACHI_AC424_TEMP_FAN, false); + break; + case HITACHI_AC424_MODE_HEAT: + case HITACHI_AC424_MODE_COOL: + case HITACHI_AC424_MODE_DRY: + break; + default: + new_mode = HITACHI_AC424_MODE_COOL; + } + set_bits(&remote_state_[HITACHI_AC424_MODE_BYTE], 0, 4, new_mode); + if (new_mode != HITACHI_AC424_MODE_FAN) + set_temp_(previous_temp_); + set_fan_(get_fan_()); // Reset the fan speed after the mode change. + set_power_(true); +} + +void HitachiClimate::set_temp_(uint8_t celsius, bool set_previous) { + uint8_t temp; + temp = std::min(celsius, HITACHI_AC424_TEMP_MAX); + temp = std::max(temp, HITACHI_AC424_TEMP_MIN); + set_bits(&remote_state_[HITACHI_AC424_TEMP_BYTE], HITACHI_AC424_TEMP_OFFSET, HITACHI_AC424_TEMP_SIZE, temp); + if (previous_temp_ > temp) + set_button_(HITACHI_AC424_BUTTON_TEMP_DOWN); + else if (previous_temp_ < temp) + set_button_(HITACHI_AC424_BUTTON_TEMP_UP); + if (set_previous) + previous_temp_ = temp; +} + +uint8_t HitachiClimate::get_fan_() { return remote_state_[HITACHI_AC424_FAN_BYTE] >> 4 & 0xF; } + +void HitachiClimate::set_fan_(uint8_t speed) { + uint8_t new_speed = std::max(speed, HITACHI_AC424_FAN_MIN); + uint8_t fan_max = HITACHI_AC424_FAN_MAX; + + // Only 2 x low speeds in Dry mode or Auto + if (get_mode_() == HITACHI_AC424_MODE_DRY && speed == HITACHI_AC424_FAN_AUTO) { + fan_max = HITACHI_AC424_FAN_AUTO; + } else if (get_mode_() == HITACHI_AC424_MODE_DRY) { + fan_max = HITACHI_AC424_FAN_MAX_DRY; + } else if (get_mode_() == HITACHI_AC424_MODE_FAN && speed == HITACHI_AC424_FAN_AUTO) { + // Fan Mode does not have auto. Set to safe low + new_speed = HITACHI_AC424_FAN_MIN; + } + + new_speed = std::min(new_speed, fan_max); + // Handle the setting the button value if we are going to change the value. + if (new_speed != get_fan_()) + set_button_(HITACHI_AC424_BUTTON_FAN); + // Set the values + + set_bits(&remote_state_[HITACHI_AC424_FAN_BYTE], 4, 4, new_speed); + remote_state_[9] = 0x92; + + // When fan is at min/max, additional bytes seem to be set + if (new_speed == HITACHI_AC424_FAN_MIN) + remote_state_[9] = 0x98; + remote_state_[29] = 0x01; +} + +void HitachiClimate::set_swing_v_toggle_(bool on) { + uint8_t button = get_button_(); // Get the current button value. + if (on) + button = HITACHI_AC424_BUTTON_SWINGV; // Set the button to SwingV. + else if (button == HITACHI_AC424_BUTTON_SWINGV) // Asked to unset it + // It was set previous, so use Power as a default + button = HITACHI_AC424_BUTTON_POWER; + set_button_(button); +} + +bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC424_BUTTON_SWINGV; } + +void HitachiClimate::set_swing_v_(bool on) { + set_swing_v_toggle_(on); // Set the button value. + set_bit(&remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET, on); +} + +bool HitachiClimate::get_swing_v_() { + return HITACHI_AC424_GETBIT8(remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET); +} + +void HitachiClimate::set_swing_h_(uint8_t position) { + if (position > HITACHI_AC424_SWINGH_LEFT_MAX) + return set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE); + set_bits(&remote_state_[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, HITACHI_AC424_SWINGH_SIZE, position); + set_button_(HITACHI_AC424_BUTTON_SWINGH); +} + +uint8_t HitachiClimate::get_swing_h_() { + return HITACHI_AC424_GETBITS8(remote_state_[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, + HITACHI_AC424_SWINGH_SIZE); +} + +uint8_t HitachiClimate::get_button_() { return remote_state_[HITACHI_AC424_BUTTON_BYTE]; } + +void HitachiClimate::set_button_(uint8_t button) { remote_state_[HITACHI_AC424_BUTTON_BYTE] = button; } + +void HitachiClimate::transmit_state() { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + set_mode_(HITACHI_AC424_MODE_COOL); + break; + case climate::CLIMATE_MODE_DRY: + set_mode_(HITACHI_AC424_MODE_DRY); + break; + case climate::CLIMATE_MODE_HEAT: + set_mode_(HITACHI_AC424_MODE_HEAT); + break; + case climate::CLIMATE_MODE_HEAT_COOL: + set_mode_(HITACHI_AC424_MODE_AUTO); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + set_mode_(HITACHI_AC424_MODE_FAN); + break; + case climate::CLIMATE_MODE_OFF: + set_power_(false); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %s", climate_mode_to_string(this->mode)); + } + + set_temp_(static_cast(this->target_temperature)); + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + set_fan_(HITACHI_AC424_FAN_LOW); + break; + case climate::CLIMATE_FAN_MEDIUM: + set_fan_(HITACHI_AC424_FAN_MEDIUM); + break; + case climate::CLIMATE_FAN_HIGH: + set_fan_(HITACHI_AC424_FAN_HIGH); + break; + case climate::CLIMATE_FAN_ON: + case climate::CLIMATE_FAN_AUTO: + default: + set_fan_(HITACHI_AC424_FAN_AUTO); + } + + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + set_swing_v_(true); + set_swing_h_(HITACHI_AC424_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_VERTICAL: + set_swing_v_(true); + set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + set_swing_v_(false); + set_swing_h_(HITACHI_AC424_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_OFF: + set_swing_v_(false); + set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE); + break; + } + + // TODO: find change value to set button, now always set to power button + set_button_(HITACHI_AC424_BUTTON_POWER); + + invert_byte_pairs(remote_state_ + 3, HITACHI_AC424_STATE_LENGTH - 3); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(HITACHI_AC424_FREQ); + + uint8_t repeat = 0; + for (uint8_t r = 0; r <= repeat; r++) { + // Header + data->item(HITACHI_AC424_HDR_MARK, HITACHI_AC424_HDR_SPACE); + // Data + for (uint8_t i : remote_state_) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(HITACHI_AC424_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? HITACHI_AC424_ONE_SPACE : HITACHI_AC424_ZERO_SPACE); + } + } + // Footer + data->item(HITACHI_AC424_BIT_MARK, HITACHI_AC424_MIN_GAP); + } + transmit.perform(); + + dump_state_("Sent", remote_state_); +} + +bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) { + uint8_t power = remote_state[HITACHI_AC424_POWER_BYTE]; + ESP_LOGV(TAG, "Power: %02X %02X", remote_state[HITACHI_AC424_POWER_BYTE], power); + uint8_t mode = remote_state[HITACHI_AC424_MODE_BYTE] & 0xF; + ESP_LOGV(TAG, "Mode: %02X %02X", remote_state[HITACHI_AC424_MODE_BYTE], mode); + if (power == HITACHI_AC424_POWER_ON) { + switch (mode) { + case HITACHI_AC424_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case HITACHI_AC424_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case HITACHI_AC424_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case HITACHI_AC424_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case HITACHI_AC424_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + return true; +} + +bool HitachiClimate::parse_temperature_(const uint8_t remote_state[]) { + uint8_t temperature = + HITACHI_AC424_GETBITS8(remote_state[HITACHI_AC424_TEMP_BYTE], HITACHI_AC424_TEMP_OFFSET, HITACHI_AC424_TEMP_SIZE); + this->target_temperature = temperature; + ESP_LOGV(TAG, "Temperature: %02X %02u %04f", remote_state[HITACHI_AC424_TEMP_BYTE], temperature, + this->target_temperature); + return true; +} + +bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) { + uint8_t fan_mode = remote_state[HITACHI_AC424_FAN_BYTE] >> 4 & 0xF; + ESP_LOGV(TAG, "Fan: %02X %02X", remote_state[HITACHI_AC424_FAN_BYTE], fan_mode); + switch (fan_mode) { + case HITACHI_AC424_FAN_MIN: + case HITACHI_AC424_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case HITACHI_AC424_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case HITACHI_AC424_FAN_HIGH: + case HITACHI_AC424_FAN_MAX: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case HITACHI_AC424_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + return true; +} + +bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) { + uint8_t swing_modeh = HITACHI_AC424_GETBITS8(remote_state[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, + HITACHI_AC424_SWINGH_SIZE); + ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC424_SWINGH_BYTE], swing_modeh); + + if ((swing_modeh & 0x7) == 0x0) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else if ((swing_modeh & 0x3) == 0x3) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } else { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } + + return true; +} + +bool HitachiClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(HITACHI_AC424_HDR_MARK, HITACHI_AC424_HDR_SPACE)) { + ESP_LOGVV(TAG, "Header fail"); + return false; + } + + uint8_t recv_state[HITACHI_AC424_STATE_LENGTH] = {0}; + // Read all bytes. + for (uint8_t pos = 0; pos < HITACHI_AC424_STATE_LENGTH; pos++) { + // Read bit + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(HITACHI_AC424_BIT_MARK, HITACHI_AC424_ONE_SPACE)) + recv_state[pos] |= 1 << bit; + else if (!data.expect_item(HITACHI_AC424_BIT_MARK, HITACHI_AC424_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Byte %d bit %d fail", pos, bit); + return false; + } + } + } + + // Validate footer + if (!data.expect_mark(HITACHI_AC424_BIT_MARK)) { + ESP_LOGVV(TAG, "Footer fail"); + return false; + } + + dump_state_("Recv", recv_state); + + // parse mode + this->parse_mode_(recv_state); + // parse temperature + this->parse_temperature_(recv_state); + // parse fan + this->parse_fan_(recv_state); + // parse swingv + this->parse_swing_(recv_state); + this->publish_state(); + for (uint8_t i = 0; i < HITACHI_AC424_STATE_LENGTH; i++) + remote_state_[i] = recv_state[i]; + + return true; +} + +void HitachiClimate::dump_state_(const char action[], uint8_t state[]) { + for (uint16_t i = 0; i < HITACHI_AC424_STATE_LENGTH - 10; i += 10) { + ESP_LOGV(TAG, "%s: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", action, state[i + 0], state[i + 1], + state[i + 2], state[i + 3], state[i + 4], state[i + 5], state[i + 6], state[i + 7], state[i + 8], + state[i + 9]); + } + ESP_LOGV(TAG, "%s: %02X %02X %02X", action, state[40], state[41], state[42]); +} + +} // namespace hitachi_ac424 +} // namespace esphome diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.h b/esphome/components/hitachi_ac424/hitachi_ac424.h new file mode 100644 index 0000000000..1005aa6df7 --- /dev/null +++ b/esphome/components/hitachi_ac424/hitachi_ac424.h @@ -0,0 +1,123 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace hitachi_ac424 { + +const uint16_t HITACHI_AC424_HDR_MARK = 3416; // ac +const uint16_t HITACHI_AC424_HDR_SPACE = 1604; // ac +const uint16_t HITACHI_AC424_BIT_MARK = 463; +const uint16_t HITACHI_AC424_ONE_SPACE = 1208; +const uint16_t HITACHI_AC424_ZERO_SPACE = 372; +const uint32_t HITACHI_AC424_MIN_GAP = 100000; // just a guess. +const uint16_t HITACHI_AC424_FREQ = 38000; // Hz. + +const uint8_t HITACHI_AC424_BUTTON_BYTE = 11; +const uint8_t HITACHI_AC424_BUTTON_POWER = 0x13; +const uint8_t HITACHI_AC424_BUTTON_SLEEP = 0x31; +const uint8_t HITACHI_AC424_BUTTON_MODE = 0x41; +const uint8_t HITACHI_AC424_BUTTON_FAN = 0x42; +const uint8_t HITACHI_AC424_BUTTON_TEMP_DOWN = 0x43; +const uint8_t HITACHI_AC424_BUTTON_TEMP_UP = 0x44; +const uint8_t HITACHI_AC424_BUTTON_SWINGV = 0x81; +const uint8_t HITACHI_AC424_BUTTON_SWINGH = 0x8C; +const uint8_t HITACHI_AC424_BUTTON_MILDEWPROOF = 0xE2; + +const uint8_t HITACHI_AC424_TEMP_BYTE = 13; +const uint8_t HITACHI_AC424_TEMP_OFFSET = 2; +const uint8_t HITACHI_AC424_TEMP_SIZE = 6; +const uint8_t HITACHI_AC424_TEMP_MIN = 16; // 16C +const uint8_t HITACHI_AC424_TEMP_MAX = 32; // 32C +const uint8_t HITACHI_AC424_TEMP_FAN = 27; // 27C + +const uint8_t HITACHI_AC424_TIMER_BYTE = 15; + +const uint8_t HITACHI_AC424_MODE_BYTE = 25; +const uint8_t HITACHI_AC424_MODE_FAN = 1; +const uint8_t HITACHI_AC424_MODE_COOL = 3; +const uint8_t HITACHI_AC424_MODE_DRY = 5; +const uint8_t HITACHI_AC424_MODE_HEAT = 6; +const uint8_t HITACHI_AC424_MODE_AUTO = 14; +const uint8_t HITACHI_AC424_MODE_POWERFUL = 19; + +const uint8_t HITACHI_AC424_FAN_BYTE = HITACHI_AC424_MODE_BYTE; +const uint8_t HITACHI_AC424_FAN_MIN = 1; +const uint8_t HITACHI_AC424_FAN_LOW = 2; +const uint8_t HITACHI_AC424_FAN_MEDIUM = 3; +const uint8_t HITACHI_AC424_FAN_HIGH = 4; +const uint8_t HITACHI_AC424_FAN_AUTO = 5; +const uint8_t HITACHI_AC424_FAN_MAX = 6; +const uint8_t HITACHI_AC424_FAN_MAX_DRY = 2; + +const uint8_t HITACHI_AC424_POWER_BYTE = 27; +const uint8_t HITACHI_AC424_POWER_ON = 0xF1; +const uint8_t HITACHI_AC424_POWER_OFF = 0xE1; + +const uint8_t HITACHI_AC424_SWINGH_BYTE = 35; +const uint8_t HITACHI_AC424_SWINGH_OFFSET = 0; // Mask 0b00000xxx +const uint8_t HITACHI_AC424_SWINGH_SIZE = 3; // Mask 0b00000xxx +const uint8_t HITACHI_AC424_SWINGH_AUTO = 0; // 0b000 +const uint8_t HITACHI_AC424_SWINGH_RIGHT_MAX = 1; // 0b001 +const uint8_t HITACHI_AC424_SWINGH_RIGHT = 2; // 0b010 +const uint8_t HITACHI_AC424_SWINGH_MIDDLE = 3; // 0b011 +const uint8_t HITACHI_AC424_SWINGH_LEFT = 4; // 0b100 +const uint8_t HITACHI_AC424_SWINGH_LEFT_MAX = 5; // 0b101 + +const uint8_t HITACHI_AC424_SWINGV_BYTE = 37; +const uint8_t HITACHI_AC424_SWINGV_OFFSET = 5; // Mask 0b00x00000 + +const uint8_t HITACHI_AC424_MILDEWPROOF_BYTE = HITACHI_AC424_SWINGV_BYTE; +const uint8_t HITACHI_AC424_MILDEWPROOF_OFFSET = 2; // Mask 0b00000x00 + +const uint16_t HITACHI_AC424_STATE_LENGTH = 53; +const uint16_t HITACHI_AC424_BITS = HITACHI_AC424_STATE_LENGTH * 8; + +#define HITACHI_AC424_GETBIT8(a, b) ((a) & ((uint8_t) 1 << (b))) +#define HITACHI_AC424_GETBITS8(data, offset, size) \ + (((data) & (((uint8_t) UINT8_MAX >> (8 - (size))) << (offset))) >> (offset)) + +class HitachiClimate : public climate_ir::ClimateIR { + public: + HitachiClimate() + : climate_ir::ClimateIR(HITACHI_AC424_TEMP_MIN, HITACHI_AC424_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_HORIZONTAL}) {} + + protected: + uint8_t remote_state_[HITACHI_AC424_STATE_LENGTH]{ + 0x01, 0x10, 0x00, 0x40, 0xBF, 0xFF, 0x00, 0xCC, 0x33, 0x92, 0x6D, 0x13, 0xEC, 0x5C, 0xA3, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x53, 0xAC, 0xF1, 0x0E, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, 0x03, + 0xFC, 0x01, 0xFE, 0x88, 0x77, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}; + uint8_t previous_temp_{27}; + // Transmit via IR the state of this climate controller. + void transmit_state() override; + bool get_power_(); + void set_power_(bool on); + uint8_t get_mode_(); + void set_mode_(uint8_t mode); + void set_temp_(uint8_t celsius, bool set_previous = false); + uint8_t get_fan_(); + void set_fan_(uint8_t speed); + void set_swing_v_toggle_(bool on); + bool get_swing_v_toggle_(); + void set_swing_v_(bool on); + bool get_swing_v_(); + void set_swing_h_(uint8_t position); + uint8_t get_swing_h_(); + uint8_t get_button_(); + void set_button_(uint8_t button); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_mode_(const uint8_t remote_state[]); + bool parse_temperature_(const uint8_t remote_state[]); + bool parse_fan_(const uint8_t remote_state[]); + bool parse_swing_(const uint8_t remote_state[]); + bool parse_state_frame_(const uint8_t frame[]); + void dump_state_(const char action[], uint8_t remote_state[]); +}; + +} // namespace hitachi_ac424 +} // namespace esphome diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index face32872e..75590f8572 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -18,7 +18,6 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -58,21 +57,29 @@ CONFIG_SCHEMA = cv.Schema( pins.internal_gpio_input_pullup_pin_schema, pins.validate_has_interrupt ), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 1, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 48a29ed5f8..fe1c6008d4 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -43,32 +42,28 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(HM3301Component), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AQI): sensor.sensor_schema( - UNIT_INDEX, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_INDEX, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Required(CONF_CALCULATION_TYPE): cv.enum( diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 65469003ed..73e7472dcf 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, - DEVICE_CLASS_EMPTY, ICON_MAGNET, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -80,10 +79,16 @@ def validate_enum(enum_values, units=None, int=True): field_strength_schema = sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) heading_schema = sensor.sensor_schema( - UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 0dadb78b73..cf29db8bb8 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -5,10 +5,7 @@ from esphome.const import ( CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID, - ICON_EMPTY, STATE_CLASS_NONE, - UNIT_EMPTY, - DEVICE_CLASS_EMPTY, ) from .. import homeassistant_ns @@ -19,7 +16,8 @@ HomeassistantSensor = homeassistant_ns.class_( ) CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ).extend( { cv.GenerateID(): cv.declare_id(HomeassistantSensor), diff --git a/esphome/components/hrxl_maxsonar_wr/__init__.py b/esphome/components/hrxl_maxsonar_wr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp new file mode 100644 index 0000000000..cf6c9eea65 --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -0,0 +1,66 @@ +// Official Datasheet: +// https://www.maxbotix.com/documents/HRXL-MaxSonar-WR_Datasheet.pdf +// +// This implementation is designed to work with the TTL Versions of the +// MaxBotix HRXL MaxSonar WR sensor series. The sensor's TTL Pin (5) should be +// wired to one of the ESP's input pins and configured as uart rx_pin. + +#include "hrxl_maxsonar_wr.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hrxl_maxsonar_wr { + +static const char *const TAG = "hrxl.maxsonar.wr.sensor"; +static const uint8_t ASCII_CR = 0x0D; +static const uint8_t ASCII_NBSP = 0xFF; +static const int MAX_DATA_LENGTH_BYTES = 6; + +/** + * The sensor outputs something like "R1234\r" at a fixed rate of 6 Hz. Where + * 1234 means a distance of 1,234 m. + */ +void HrxlMaxsonarWrComponent::loop() { + uint8_t data; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_ += (char) data; + this->check_buffer_(); + } + } +} + +void HrxlMaxsonarWrComponent::check_buffer_() { + // The sensor seems to inject a rogue ASCII 255 byte from time to time. Get rid of that. + if (this->buffer_.back() == static_cast(ASCII_NBSP)) { + this->buffer_.pop_back(); + return; + } + + // Stop reading at ASCII_CR. Also prevent the buffer from growing + // indefinitely if no ASCII_CR is received after MAX_DATA_LENGTH_BYTES. + if (this->buffer_.back() == static_cast(ASCII_CR) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) { + ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.c_str()); + + if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && + this->buffer_.back() == static_cast(ASCII_CR)) { + int millimeters = strtol(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2).c_str(), nullptr, 10); + float meters = float(millimeters) / 1000.0; + ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); + this->publish_state(meters); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", this->buffer_.c_str()); + } + this->buffer_.clear(); + } +} + +void HrxlMaxsonarWrComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:"); + LOG_SENSOR(" ", "Distance", this); + // As specified in the sensor's data sheet + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); +} + +} // namespace hrxl_maxsonar_wr +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h new file mode 100644 index 0000000000..efb8bc5f4b --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace hrxl_maxsonar_wr { + +class HrxlMaxsonarWrComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::string buffer_; +}; + +} // namespace hrxl_maxsonar_wr +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py new file mode 100644 index 0000000000..dd43bd84a7 --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, +) + +CODEOWNERS = ["@netmikey"] +DEPENDENCIES = ["uart"] + +hrxlmaxsonarwr_ns = cg.esphome_ns.namespace("hrxl_maxsonar_wr") +HrxlMaxsonarWrComponent = hrxlmaxsonarwr_ns.class_( + "HrxlMaxsonarWrComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(HrxlMaxsonarWrComponent), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 435c5bf1bb..37422f0329 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(HTU21DComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hx711/sensor.py b/esphome/components/hx711/sensor.py index 17a4e35d5f..cd06cc770f 100644 --- a/esphome/components/hx711/sensor.py +++ b/esphome/components/hx711/sensor.py @@ -6,10 +6,8 @@ from esphome.const import ( CONF_CLK_PIN, CONF_GAIN, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_SCALE, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) hx711_ns = cg.esphome_ns.namespace("hx711") @@ -26,7 +24,9 @@ GAINS = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_SCALE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_SCALE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index e973671acc..c06d487e7d 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -225,7 +225,7 @@ void ILI9341M5Stack::initialize() { this->width_ = 320; this->height_ = 240; this->invert_display_(true); - this->fill_internal_(COLOR_BLACK); + this->fill_internal_(Color::BLACK); } // 24_TFT display @@ -233,7 +233,7 @@ void ILI9341TFT24::initialize() { this->init_lcd_(INITCMD_TFT); this->width_ = 240; this->height_ = 320; - this->fill_internal_(COLOR_BLACK); + this->fill_internal_(Color::BLACK); } } // namespace ili9341 diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py index ed88ace967..020be9bc6e 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -13,7 +13,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,20 +31,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(INA219Component), cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0, max=32.0) diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index e4ceda39c1..ee4036ce7e 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -31,20 +30,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(INA226Component), cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0) diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 8b861d972d..9c42ecbb9d 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,16 +31,28 @@ INA3221Component = ina3221_ns.class_( INA3221_CHANNEL_SCHEMA = cv.Schema( { cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0, max=32.0) diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index 45be7c1acf..a940a77148 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -23,9 +23,9 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi // for Inkbird IBS-TH1 Mini device we expect // 1) expected mac address // 2) device address type == PUBLIC - // 3) no service datas - // 4) one manufacturer datas - // 5) the manufacturer datas should contain a 16-bit uuid amd a 7-byte data vector + // 3) no service data + // 4) one manufacturer data + // 5) the manufacturer data should contain a 16-bit uuid amd a 7-byte data vector // 6) the 7-byte data component should have data[2] == 0 and data[6] == 8 // the address should match the address we declared @@ -63,7 +63,7 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi // sensor output encoding // data[5] is a battery level // data[0] and data[1] is humidity * 100 (in pct) - // uuid is a temperature * 100 (in Celcius) + // uuid is a temperature * 100 (in Celsius) // when data[2] == 0 temperature is from internal sensor (IBS-TH1 or IBS-TH1 Mini) // when data[2] == 1 temperature is from external sensor (IBS-TH1 only) diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index aaaaddb890..a71921f8ed 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,32 +31,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(InkbirdUBSTH1_MINI), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index bfcf8d3561..9a0f9fc58b 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -16,6 +16,8 @@ void IntegrationSensor::setup() { } this->last_update_ = millis(); + this->last_save_ = this->last_update_; + this->publish_and_save_(this->result_); this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); }); } diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 2fcec069b2..c575de094c 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -27,6 +27,7 @@ class IntegrationSensor : public sensor::Sensor, public Component { void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } + void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } @@ -55,6 +56,10 @@ class IntegrationSensor : public sensor::Sensor, public Component { this->result_ = result; this->publish_state(result); float result_f = result; + const uint32_t now = millis(); + if (now - this->last_save_ < this->min_save_interval_) + return; + this->last_save_ = now; this->rtc_.save(&result_f); } std::string unit_of_measurement() override; @@ -67,6 +72,8 @@ class IntegrationSensor : public sensor::Sensor, public Component { bool restore_; ESPPreferenceObject rtc_; + uint32_t last_save_{0}; + uint32_t min_save_interval_{0}; uint32_t last_update_; double result_{0.0f}; float last_value_{0.0f}; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 3f32394ff6..460dd46619 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -27,6 +27,8 @@ INTEGRATION_METHODS = { CONF_TIME_UNIT = "time_unit" CONF_INTEGRATION_METHOD = "integration_method" +CONF_MIN_SAVE_INTERVAL = "min_save_interval" + CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( { @@ -37,6 +39,9 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( INTEGRATION_METHODS, lower=True ), cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional( + CONF_MIN_SAVE_INTERVAL, default="0s" + ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -52,6 +57,7 @@ async def to_code(config): cg.add(var.set_time(config[CONF_TIME_UNIT])) cg.add(var.set_method(config[CONF_INTEGRATION_METHOD])) cg.add(var.set_restore(config[CONF_RESTORE])) + cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) @automation.register_action( diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 119ff3703c..52e8984545 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -5,6 +5,7 @@ from esphome.components import mqtt, power_supply from esphome.const import ( CONF_COLOR_CORRECT, CONF_DEFAULT_TRANSITION_LENGTH, + CONF_DISABLED_BY_DEFAULT, CONF_EFFECTS, CONF_GAMMA_CORRECT, CONF_ID, @@ -52,7 +53,7 @@ RESTORE_MODES = { "RESTORE_INVERTED_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_ON, } -LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +LIGHT_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(LightState), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTJSONLightComponent), @@ -108,7 +109,9 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( def validate_color_temperature_channels(value): if ( - value[CONF_COLD_WHITE_COLOR_TEMPERATURE] + CONF_COLD_WHITE_COLOR_TEMPERATURE in value + and CONF_WARM_WHITE_COLOR_TEMPERATURE in value + and value[CONF_COLD_WHITE_COLOR_TEMPERATURE] >= value[CONF_WARM_WHITE_COLOR_TEMPERATURE] ): raise cv.Invalid( @@ -119,6 +122,7 @@ def validate_color_temperature_channels(value): async def setup_light_core_(light_var, output_var, config): + cg.add(light_var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if CONF_INTERNAL in config: cg.add(light_var.set_internal(config[CONF_INTERNAL])) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 4ef293cd03..12eab6a685 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -24,79 +24,84 @@ void AddressableLight::call_setup() { #endif } +std::unique_ptr AddressableLight::create_default_transition() { + return make_unique(*this); +} + Color esp_color_from_light_color_values(LightColorValues val) { - auto r = static_cast(roundf(val.get_color_brightness() * val.get_red() * 255.0f)); - auto g = static_cast(roundf(val.get_color_brightness() * val.get_green() * 255.0f)); - auto b = static_cast(roundf(val.get_color_brightness() * val.get_blue() * 255.0f)); - auto w = static_cast(roundf(val.get_white() * 255.0f)); + auto r = to_uint8_scale(val.get_color_brightness() * val.get_red()); + auto g = to_uint8_scale(val.get_color_brightness() * val.get_green()); + auto b = to_uint8_scale(val.get_color_brightness() * val.get_blue()); + auto w = to_uint8_scale(val.get_white()); return Color(r, g, b, w); } void AddressableLight::write_state(LightState *state) { auto val = state->current_values; - auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); + auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); this->correction_.set_local_brightness(max_brightness); - this->last_transition_progress_ = 0.0f; - this->accumulated_alpha_ = 0.0f; - if (this->is_effect_active()) return; // don't use LightState helper, gamma correction+brightness is handled by ESPColorView + this->all() = esp_color_from_light_color_values(val); +} - if (state->transformer_ == nullptr || !state->transformer_->is_transition()) { - // no transformer active or non-transition one - this->all() = esp_color_from_light_color_values(val); - } else { - // transition transformer active, activate specialized transition for addressable effects - // instead of using a unified transition for all LEDs, we use the current state each LED as the - // start. Warning: ugly +void AddressableLightTransformer::start() { + auto end_values = this->target_values_; + this->target_color_ = esp_color_from_light_color_values(end_values); - // We can't use a direct lerp smoothing here though - that would require creating a copy of the original - // state of each LED at the start of the transition - // Instead, we "fake" the look of the LERP by using an exponential average over time and using - // dynamically-calculated alpha values to match the look of the + // our transition will handle brightness, disable brightness in correction. + this->light_.correction_.set_local_brightness(255); + this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); +} - float new_progress = state->transformer_->get_progress(); - float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_); - float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress); - this->last_transition_progress_ = new_progress; +optional AddressableLightTransformer::apply() { + // Don't try to transition over running effects, instead immediately use the target values. write_state() and the + // effects pick up the change from current_values. + if (this->light_.is_effect_active()) + return this->target_values_; - auto end_values = state->transformer_->get_end_values(); - Color target_color = esp_color_from_light_color_values(end_values); + // Use a specialized transition for addressable lights: instead of using a unified transition for + // all LEDs, we use the current state of each LED as the start. - // our transition will handle brightness, disable brightness in correction. - this->correction_.set_local_brightness(255); - target_color *= static_cast(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); + // We can't use a direct lerp smoothing here though - that would require creating a copy of the original + // state of each LED at the start of the transition. + // Instead, we "fake" the look of the LERP by using an exponential average over time and using + // dynamically-calculated alpha values to match the look. - float denom = (1.0f - new_smoothed); - float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; + float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_()); - // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length - // We solve this by accumulating the fractional part of the alpha over time. - float alpha255 = alpha * 255.0f; - float alpha255int = floorf(alpha255); - float alpha255remainder = alpha255 - alpha255int; + float denom = (1.0f - smoothed_progress); + float alpha = denom == 0.0f ? 0.0f : (smoothed_progress - this->last_transition_progress_) / denom; - this->accumulated_alpha_ += alpha255remainder; - float alpha_add = floorf(this->accumulated_alpha_); - this->accumulated_alpha_ -= alpha_add; + // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length + // We solve this by accumulating the fractional part of the alpha over time. + float alpha255 = alpha * 255.0f; + float alpha255int = floorf(alpha255); + float alpha255remainder = alpha255 - alpha255int; - alpha255 += alpha_add; - alpha255 = clamp(alpha255, 0.0f, 255.0f); - auto alpha8 = static_cast(alpha255); + this->accumulated_alpha_ += alpha255remainder; + float alpha_add = floorf(this->accumulated_alpha_); + this->accumulated_alpha_ -= alpha_add; - if (alpha8 != 0) { - uint8_t inv_alpha8 = 255 - alpha8; - Color add = target_color * alpha8; + alpha255 += alpha_add; + alpha255 = clamp(alpha255, 0.0f, 255.0f); + auto alpha8 = static_cast(alpha255); - for (auto led : *this) - led = add + led.get() * inv_alpha8; - } + if (alpha8 != 0) { + uint8_t inv_alpha8 = 255 - alpha8; + Color add = this->target_color_ * alpha8; + + for (auto led : this->light_) + led.set(add + led.get() * inv_alpha8); } - this->schedule_show(); + this->last_transition_progress_ = smoothed_progress; + this->light_.schedule_show(); + + return {}; } } // namespace light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 460cb88935..ab1efdf160 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -8,6 +8,7 @@ #include "esp_range_view.h" #include "light_output.h" #include "light_state.h" +#include "transformers.h" #ifdef USE_POWER_SUPPLY #include "esphome/components/power_supply/power_supply.h" @@ -16,7 +17,7 @@ namespace esphome { namespace light { -using ESPColor = Color; +using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color; class AddressableLight : public LightOutput, public Component { public: @@ -53,9 +54,10 @@ class AddressableLight : public LightOutput, public Component { bool is_effect_active() const { return this->effect_active_; } void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } void write_state(LightState *state) override; + std::unique_ptr create_default_transition() override; void set_correction(float red, float green, float blue, float white = 1.0f) { - this->correction_.set_max_brightness(Color(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)), - uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f)))); + this->correction_.set_max_brightness( + Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white))); } void setup_state(LightState *state) override { this->correction_.calculate_gamma_table(state->get_gamma_correct()); @@ -70,6 +72,8 @@ class AddressableLight : public LightOutput, public Component { void call_setup() override; protected: + friend class AddressableLightTransformer; + bool should_show_() const { return this->effect_active_ || this->next_show_; } void mark_shown_() { this->next_show_ = false; @@ -92,6 +96,18 @@ class AddressableLight : public LightOutput, public Component { power_supply::PowerSupplyRequester power_; #endif LightState *state_parent_{nullptr}; +}; + +class AddressableLightTransformer : public LightTransitionTransformer { + public: + AddressableLightTransformer(AddressableLight &light) : light_(light) {} + + void start() override; + optional apply() override; + + protected: + AddressableLight &light_; + Color target_color_{}; float last_transition_progress_{0.0f}; float accumulated_alpha_{0.0f}; }; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index d1ea9e3ff0..3a2ba66845 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -151,7 +151,7 @@ class AddressableScanEffect : public AddressableLightEffect { void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const Color ¤t_color) override { - it.all() = COLOR_BLACK; + it.all() = Color::BLACK; for (auto i = 0; i < this->scan_width_; i++) { it[this->at_led_ + i] = current_color; @@ -201,7 +201,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { else view.set_effect_data(new_pos); } else { - view = COLOR_BLACK; + view = Color::BLACK; } } while (random_float() < this->twinkle_probability_) { @@ -272,7 +272,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} void start() override { auto &it = *this->get_addressable_(); - it.all() = COLOR_BLACK; + it.all() = Color::BLACK; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); @@ -337,7 +337,7 @@ class AddressableFlickerEffect : public AddressableLightEffect { } } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } - void set_intensity(float intensity) { this->intensity_ = static_cast(roundf(intensity * 255.0f)); } + void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } protected: uint32_t update_interval_{16}; diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp new file mode 100644 index 0000000000..8c1785f061 --- /dev/null +++ b/esphome/components/light/automation.cpp @@ -0,0 +1,15 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +static const char *const TAG = "light.automation"; + +void addressableset_warn_about_scale(const char *field) { + ESP_LOGW(TAG, "Lambda for parameter %s of light.addressable_set should return values in range 0-1 instead of 0-255.", + field); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index e99d5c58bd..5ec2cb626a 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -27,6 +27,7 @@ template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(ColorMode, color_mode) TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(uint32_t, transition_length) TEMPLATABLE_VALUE(uint32_t, flash_length) @@ -37,10 +38,13 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) TEMPLATABLE_VALUE(float, color_temperature) + TEMPLATABLE_VALUE(float, cold_white) + TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) void play(Ts... x) override { auto call = this->parent_->make_call(); + call.set_color_mode(this->color_mode_.optional_value(x...)); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); call.set_color_brightness(this->color_brightness_.optional_value(x...)); @@ -49,6 +53,8 @@ template class LightControlAction : public Action { call.set_blue(this->blue_.optional_value(x...)); call.set_white(this->white_.optional_value(x...)); call.set_color_temperature(this->color_temperature_.optional_value(x...)); + call.set_cold_white(this->cold_white_.optional_value(x...)); + call.set_warm_white(this->warm_white_.optional_value(x...)); call.set_effect(this->effect_.optional_value(x...)); call.set_flash_length(this->flash_length_.optional_value(x...)); call.set_transition_length(this->transition_length_.optional_value(x...)); @@ -135,39 +141,57 @@ class LightTurnOffTrigger : public Trigger<> { } }; +// This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet +// due to the template. It's just a temporary warning anyway. +void addressableset_warn_about_scale(const char *field); + template class AddressableSet : public Action { public: explicit AddressableSet(LightState *parent) : parent_(parent) {} TEMPLATABLE_VALUE(int32_t, range_from) TEMPLATABLE_VALUE(int32_t, range_to) - TEMPLATABLE_VALUE(uint8_t, color_brightness) - TEMPLATABLE_VALUE(uint8_t, red) - TEMPLATABLE_VALUE(uint8_t, green) - TEMPLATABLE_VALUE(uint8_t, blue) - TEMPLATABLE_VALUE(uint8_t, white) + TEMPLATABLE_VALUE(float, color_brightness) + TEMPLATABLE_VALUE(float, red) + TEMPLATABLE_VALUE(float, green) + TEMPLATABLE_VALUE(float, blue) + TEMPLATABLE_VALUE(float, white) void play(Ts... x) override { auto *out = (AddressableLight *) this->parent_->get_output(); - int32_t range_from = this->range_from_.value_or(x..., 0); - int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; - uint8_t remote_color_brightness = - static_cast(roundf(this->parent_->remote_values.get_color_brightness() * 255.0f)); - uint8_t color_brightness = this->color_brightness_.value_or(x..., remote_color_brightness); + int32_t range_from = interpret_index(this->range_from_.value_or(x..., 0), out->size()); + if (range_from < 0 || range_from >= out->size()) + range_from = 0; + + int32_t range_to = interpret_index(this->range_to_.value_or(x..., out->size() - 1) + 1, out->size()); + if (range_to < 0 || range_to >= out->size()) + range_to = out->size(); + + uint8_t color_brightness = + to_uint8_scale(this->color_brightness_.value_or(x..., this->parent_->remote_values.get_color_brightness())); auto range = out->range(range_from, range_to); if (this->red_.has_value()) - range.set_red(esp_scale8(this->red_.value(x...), color_brightness)); + range.set_red(esp_scale8(to_uint8_compat(this->red_.value(x...), "red"), color_brightness)); if (this->green_.has_value()) - range.set_green(esp_scale8(this->green_.value(x...), color_brightness)); + range.set_green(esp_scale8(to_uint8_compat(this->green_.value(x...), "green"), color_brightness)); if (this->blue_.has_value()) - range.set_blue(esp_scale8(this->blue_.value(x...), color_brightness)); + range.set_blue(esp_scale8(to_uint8_compat(this->blue_.value(x...), "blue"), color_brightness)); if (this->white_.has_value()) - range.set_white(this->white_.value(x...)); + range.set_white(to_uint8_compat(this->white_.value(x...), "white")); out->schedule_show(); } protected: LightState *parent_; + + // Historically, this action required uint8_t (0-255) for RGBW values from lambdas. Keep compatibility. + static inline uint8_t to_uint8_compat(float value, const char *field) { + if (value > 1.0f) { + addressableset_warn_about_scale(field); + return static_cast(value); + } + return to_uint8_scale(value); + } }; } // namespace light diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index dd1148131a..cfba273565 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import ( CONF_ID, + CONF_COLOR_MODE, CONF_TRANSITION_LENGTH, CONF_STATE, CONF_FLASH_LENGTH, @@ -14,10 +15,14 @@ from esphome.const import ( CONF_BLUE, CONF_WHITE, CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, CONF_RANGE_FROM, CONF_RANGE_TO, ) from .types import ( + ColorMode, + COLOR_MODES, DimRelativeAction, ToggleAction, LightState, @@ -55,6 +60,7 @@ async def light_toggle_to_code(config, action_id, template_arg, args): LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_COLOR_MODE): cv.enum(COLOR_MODES, upper=True, space="_"), cv.Optional(CONF_STATE): cv.templatable(cv.boolean), cv.Exclusive(CONF_TRANSITION_LENGTH, "transformer"): cv.templatable( cv.positive_time_period_milliseconds @@ -70,6 +76,8 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature), + cv.Optional(CONF_COLD_WHITE): cv.templatable(cv.percentage), + cv.Optional(CONF_WARM_WHITE): cv.templatable(cv.percentage), } ) LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( @@ -102,6 +110,9 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def light_control_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_COLOR_MODE in config: + template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) + cg.add(var.set_color_mode(template_)) if CONF_STATE in config: template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) @@ -134,6 +145,12 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_COLOR_TEMPERATURE in config: template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) cg.add(var.set_color_temperature(template_)) + if CONF_COLD_WHITE in config: + template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) + cg.add(var.set_cold_white(template_)) + if CONF_WARM_WHITE in config: + template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) + cg.add(var.set_warm_white(template_)) if CONF_EFFECT in config: template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) cg.add(var.set_effect(template_)) @@ -173,6 +190,7 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema( cv.Required(CONF_ID): cv.use_id(AddressableLightState), cv.Optional(CONF_RANGE_FROM): cv.templatable(cv.positive_int), cv.Optional(CONF_RANGE_TO): cv.templatable(cv.positive_int), + cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_RED): cv.templatable(cv.percentage), cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), @@ -194,28 +212,20 @@ async def light_addressable_set_to_code(config, action_id, template_arg, args): templ = await cg.templatable(config[CONF_RANGE_TO], args, cg.int32) cg.add(var.set_range_to(templ)) - def rgbw_to_exp(x): - return int(round(x * 255)) - + if CONF_COLOR_BRIGHTNESS in config: + templ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, cg.float_) + cg.add(var.set_color_brightness(templ)) if CONF_RED in config: - templ = await cg.templatable( - config[CONF_RED], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_RED], args, cg.float_) cg.add(var.set_red(templ)) if CONF_GREEN in config: - templ = await cg.templatable( - config[CONF_GREEN], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_GREEN], args, cg.float_) cg.add(var.set_green(templ)) if CONF_BLUE in config: - templ = await cg.templatable( - config[CONF_BLUE], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_BLUE], args, cg.float_) cg.add(var.set_blue(templ)) if CONF_WHITE in config: - templ = await cg.templatable( - config[CONF_WHITE], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_WHITE], args, cg.float_) cg.add(var.set_white(templ)) return var diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index eb92fec642..f66b90f665 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -57,16 +57,31 @@ class RandomLightEffect : public LightEffect { if (now - this->last_color_change_ < this->update_interval_) { return; } + + auto color_mode = this->state_->remote_values.get_color_mode(); auto call = this->state_->turn_on(); - if (this->state_->get_traits().get_supports_rgb()) { - call.set_red_if_supported(random_float()); - call.set_green_if_supported(random_float()); - call.set_blue_if_supported(random_float()); - call.set_white_if_supported(random_float()); - } else { - call.set_brightness_if_supported(random_float()); + bool changed = false; + if (color_mode & ColorCapability::RGB) { + call.set_red(random_float()); + call.set_green(random_float()); + call.set_blue(random_float()); + changed = true; + } + if (color_mode & ColorCapability::COLOR_TEMPERATURE) { + float min = this->state_->get_traits().get_min_mireds(); + float max = this->state_->get_traits().get_max_mireds(); + call.set_color_temperature(min + random_float() * (max - min)); + changed = true; + } + if (color_mode & ColorCapability::COLD_WARM_WHITE) { + call.set_cold_white(random_float()); + call.set_warm_white(random_float()); + changed = true; + } + if (!changed) { + // only randomize brightness if there's no colored option available + call.set_brightness(random_float()); } - call.set_color_temperature_if_supported(random_float()); call.set_transition_length_if_supported(this->transition_length_); call.set_publish(true); call.set_save(false); @@ -142,7 +157,6 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped call.set_brightness_if_supported(0.0f); - call.set_white_if_supported(0.0f); call.set_state(true); } call.set_publish(false); @@ -177,13 +191,16 @@ class FlickerLightEffect : public LightEffect { out.set_green(remote.get_green() * beta + current.get_green() * alpha + (random_cubic_float() * this->intensity_)); out.set_blue(remote.get_blue() * beta + current.get_blue() * alpha + (random_cubic_float() * this->intensity_)); out.set_white(remote.get_white() * beta + current.get_white() * alpha + (random_cubic_float() * this->intensity_)); + out.set_cold_white(remote.get_cold_white() * beta + current.get_cold_white() * alpha + + (random_cubic_float() * this->intensity_)); + out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + + (random_cubic_float() * this->intensity_)); auto traits = this->state_->get_traits(); auto call = this->state_->make_call(); call.set_publish(false); call.set_save(false); - if (traits.get_supports_brightness()) - call.set_transition_length(0); + call.set_transition_length_if_supported(0); call.from_light_color_values(out); call.set_state(true); call.perform(); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h new file mode 100644 index 0000000000..0f5b7b4b93 --- /dev/null +++ b/esphome/components/light/color_mode.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +namespace esphome { +namespace light { + +/// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. +enum class ColorCapability : uint8_t { + /// Light can be turned on/off. + ON_OFF = 1 << 0, + /// Master brightness of the light can be controlled. + BRIGHTNESS = 1 << 1, + /// Brightness of white channel can be controlled separately from other channels. + WHITE = 1 << 2, + /// Color temperature can be controlled. + COLOR_TEMPERATURE = 1 << 3, + /// Brightness of cold and warm white output can be controlled. + COLD_WARM_WHITE = 1 << 4, + /// Color can be controlled using RGB format (includes a brightness control for the color). + RGB = 1 << 5 +}; + +/// Helper class to allow bitwise operations on ColorCapability +class ColorCapabilityHelper { + public: + constexpr ColorCapabilityHelper(ColorCapability val) : val_(val) {} + constexpr operator ColorCapability() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorCapability val_; +}; +constexpr ColorCapabilityHelper operator&(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator&(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +/// Color modes are a combination of color capabilities that can be used at the same time. +enum class ColorMode : uint8_t { + /// No color mode configured (cannot be a supported mode, only active when light is off). + UNKNOWN = 0, + /// Only on/off control. + ON_OFF = (uint8_t) ColorCapability::ON_OFF, + /// Dimmable light. + BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, + /// White output only (use only if the light also has another color mode such as RGB). + WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), + /// Controllable color temperature output. + COLOR_TEMPERATURE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLOR_TEMPERATURE), + /// Cold and warm white output with individually controllable brightness. + COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLD_WARM_WHITE), + /// RGB color output. + RGB = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB), + /// RGB color output and a separate white output. + RGB_WHITE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | ColorCapability::WHITE), + /// RGB color output and a separate white output with controllable color temperature. + RGB_COLOR_TEMPERATURE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::WHITE | ColorCapability::COLOR_TEMPERATURE), + /// RGB color output, and separate cold and warm white outputs. + RGB_COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::COLD_WARM_WHITE), +}; + +/// Helper class to allow bitwise operations on ColorMode with ColorCapability +class ColorModeHelper { + public: + constexpr ColorModeHelper(ColorMode val) : val_(val) {} + constexpr operator ColorMode() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorMode val_; +}; +constexpr ColorModeHelper operator&(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index f6ce812c34..be558a8140 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -12,11 +12,15 @@ from esphome.const import ( CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, + CONF_COLOR_MODE, CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, CONF_ALPHA, CONF_INTENSITY, CONF_SPEED, @@ -27,6 +31,8 @@ from esphome.const import ( ) from esphome.util import Registry from .types import ( + ColorMode, + COLOR_MODES, LambdaLightEffect, PulseLightEffect, RandomLightEffect, @@ -212,11 +218,17 @@ async def random_effect_to_code(config, effect_id): { cv.Optional(CONF_STATE, default=True): cv.boolean, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_MODE): cv.enum( + COLOR_MODES, upper=True, space="_" + ), cv.Optional(CONF_COLOR_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, cv.Optional(CONF_BLUE, default=1.0): cv.percentage, cv.Optional(CONF_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_WARM_WHITE, default=1.0): cv.percentage, cv.Required( CONF_DURATION ): cv.positive_time_period_milliseconds, @@ -225,11 +237,15 @@ async def random_effect_to_code(config, effect_id): cv.has_at_least_one_key( CONF_STATE, CONF_BRIGHTNESS, + CONF_COLOR_MODE, CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, ), ), cv.Length(min=2), @@ -246,6 +262,7 @@ async def strobe_effect_to_code(config, effect_id): ( "color", LightColorValues( + color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), color[CONF_STATE], color[CONF_BRIGHTNESS], color[CONF_COLOR_BRIGHTNESS], @@ -253,6 +270,9 @@ async def strobe_effect_to_code(config, effect_id): color[CONF_GREEN], color[CONF_BLUE], color[CONF_WHITE], + color.get(CONF_COLOR_TEMPERATURE, 0.0), + color[CONF_COLD_WHITE], + color[CONF_WARM_WHITE], ), ), ("duration", color[CONF_DURATION]), diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index 19a2af3da1..e5e68264cc 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -1,4 +1,5 @@ #include "esp_color_correction.h" +#include "light_color_values.h" #include "esphome/core/log.h" namespace esphome { @@ -7,7 +8,7 @@ namespace light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { // corrected = val ^ gamma - auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); + auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma)); this->gamma_table_[i] = corrected; } if (gamma == 0.0f) { @@ -17,7 +18,7 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } for (uint16_t i = 0; i < 256; i++) { // val = corrected ^ (1/gamma) - auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); + auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma)); this->gamma_reverse_table_[i] = uncorrected; } } diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index f2cc347176..f4a7980543 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -11,6 +11,9 @@ int32_t interpret_index(int32_t index, int32_t size); class AddressableLight; class ESPRangeIterator; +/** + * A half-open range of LEDs, inclusive of the begin index and exclusive of the end index, using zero-based numbering. + */ class ESPRangeView : public ESPColorSettable { public: ESPRangeView(AddressableLight *parent, int32_t begin, int32_t end) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index d7e8ce6298..6945d37ded 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -7,95 +7,42 @@ namespace light { static const char *const TAG = "light"; -#ifdef USE_JSON -LightCall &LightCall::parse_color_json(JsonObject &root) { - if (root.containsKey("state")) { - auto val = parse_on_off(root["state"]); - switch (val) { - case PARSE_ON: - this->set_state(true); - break; - case PARSE_OFF: - this->set_state(false); - break; - case PARSE_TOGGLE: - this->set_state(!this->parent_->remote_values.is_on()); - break; - case PARSE_NONE: - break; - } +static const char *color_mode_to_human(ColorMode color_mode) { + switch (color_mode) { + case ColorMode::UNKNOWN: + return "Unknown"; + case ColorMode::WHITE: + return "White"; + case ColorMode::COLOR_TEMPERATURE: + return "Color temperature"; + case ColorMode::COLD_WARM_WHITE: + return "Cold/warm white"; + case ColorMode::RGB: + return "RGB"; + case ColorMode::RGB_WHITE: + return "RGBW"; + case ColorMode::RGB_COLD_WARM_WHITE: + return "RGB + cold/warm white"; + case ColorMode::RGB_COLOR_TEMPERATURE: + return "RGB + color temperature"; + default: + return ""; } - - if (root.containsKey("brightness")) { - this->set_brightness(float(root["brightness"]) / 255.0f); - } - - if (root.containsKey("color")) { - JsonObject &color = root["color"]; - // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. - float max_rgb = 0.0f; - if (color.containsKey("r")) { - float r = float(color["r"]) / 255.0f; - max_rgb = fmaxf(max_rgb, r); - this->set_red(r); - } - if (color.containsKey("g")) { - float g = float(color["g"]) / 255.0f; - max_rgb = fmaxf(max_rgb, g); - this->set_green(g); - } - if (color.containsKey("b")) { - float b = float(color["b"]) / 255.0f; - max_rgb = fmaxf(max_rgb, b); - this->set_blue(b); - } - if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { - this->set_color_brightness(max_rgb); - } - } - - if (root.containsKey("white_value")) { - this->set_white(float(root["white_value"]) / 255.0f); - } - - if (root.containsKey("color_temp")) { - this->set_color_temperature(float(root["color_temp"])); - } - - return *this; } -LightCall &LightCall::parse_json(JsonObject &root) { - this->parse_color_json(root); - - if (root.containsKey("flash")) { - auto length = uint32_t(float(root["flash"]) * 1000); - this->set_flash_length(length); - } - - if (root.containsKey("transition")) { - auto length = uint32_t(float(root["transition"]) * 1000); - this->set_transition_length(length); - } - - if (root.containsKey("effect")) { - const char *effect = root["effect"]; - this->set_effect(effect); - } - - return *this; -} -#endif void LightCall::perform() { - // use remote values for fallback const char *name = this->parent_->get_name().c_str(); - if (this->publish_) { - ESP_LOGD(TAG, "'%s' Setting:", name); - } - LightColorValues v = this->validate_(); if (this->publish_) { + ESP_LOGD(TAG, "'%s' Setting:", name); + + // Only print color mode when it's being changed + ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); + if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { + ESP_LOGD(TAG, " Color mode: %s", color_mode_to_human(v.get_color_mode())); + } + // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); if (this->state_.value_or(current_state) != current_state) { @@ -106,30 +53,38 @@ void LightCall::perform() { ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); } - if (this->color_temperature_.has_value()) { - ESP_LOGD(TAG, " Color Temperature: %.1f mireds", v.get_color_temperature()); + if (this->color_brightness_.has_value()) { + ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f); } - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - ESP_LOGD(TAG, " Red=%.0f%%, Green=%.0f%%, Blue=%.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } + if (this->white_.has_value()) { - ESP_LOGD(TAG, " White Value: %.0f%%", v.get_white() * 100.0f); + ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); + } + if (this->color_temperature_.has_value()) { + ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + } + + if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + v.get_warm_white() * 100.0f); } } if (this->has_flash_()) { // FLASH if (this->publish_) { - ESP_LOGD(TAG, " Flash Length: %.1fs", *this->flash_length_ / 1e3f); + ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); } this->parent_->start_flash_(v, *this->flash_length_); } else if (this->has_transition_()) { // TRANSITION if (this->publish_) { - ESP_LOGD(TAG, " Transition Length: %.1fs", *this->transition_length_ / 1e3f); + ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off @@ -177,32 +132,49 @@ void LightCall::perform() { } LightColorValues LightCall::validate_() { - // use remote values for fallback auto *name = this->parent_->get_name().c_str(); auto traits = this->parent_->get_traits(); + // Color mode check + if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { + ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + color_mode_to_human(this->color_mode_.value())); + this->color_mode_.reset(); + } + + // Ensure there is always a color mode set + if (!this->color_mode_.has_value()) { + this->color_mode_ = this->compute_color_mode_(); + } + auto color_mode = *this->color_mode_; + + // Transform calls that use non-native parameters for the current mode. + this->transform_parameters_(); + // Brightness exists check - if (this->brightness_.has_value() && !traits.get_supports_brightness()) { + if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); this->brightness_.reset(); } // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !traits.get_supports_brightness()) { + if (this->transition_length_.has_value() && *this->transition_length_ != 0 && + !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); this->transition_length_.reset(); } // Color brightness exists check - if (this->color_brightness_.has_value() && !traits.get_supports_rgb()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB brightness!", name); + if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); this->color_brightness_.reset(); } // RGB exists check - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!traits.get_supports_rgb()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB color!", name); + if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || + (this->blue_.has_value() && *this->blue_ > 0.0f)) { + if (!(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -210,80 +182,38 @@ LightColorValues LightCall::validate_() { } // White value exists check - if (this->white_.has_value() && !traits.get_supports_rgb_white_value()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting white value!", name); + if (this->white_.has_value() && *this->white_ > 0.0f && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); this->white_.reset(); } // Color temperature exists check - if (this->color_temperature_.has_value() && !traits.get_supports_color_temperature()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting color temperature!", name); + if (this->color_temperature_.has_value() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); this->color_temperature_.reset(); } - // Set color brightness to 100% if currently zero and a color is set. This is both for compatibility with older - // clients that don't know about color brightness, and it's intuitive UX anyway: if I set a color, it should show up. - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) - this->color_brightness_ = optional(1.0f); - } - - // Handle interaction between RGB and white for color interlock - if (traits.get_supports_color_interlock()) { - // Find out which channel (white or color) the user wanted to enable - bool output_white = this->white_.has_value() && *this->white_ > 0.0f; - bool output_color = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || - this->red_.has_value() || this->green_.has_value() || this->blue_.has_value(); - - // Interpret setting the color to white as setting the white channel. - if (output_color && *this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { - output_white = true; - output_color = false; - - if (!this->white_.has_value()) - this->white_ = optional(this->color_brightness_.value_or(1.0f)); - } - - // Ensure either the white value or the color brightness is always zero. - if (output_white && output_color) { - ESP_LOGW(TAG, "'%s' - Cannot enable color and white channel simultaneously with interlock!", name); - // For compatibility with historic behaviour, prefer white channel in this case. - this->color_brightness_ = optional(0.0f); - } else if (output_white) { - this->color_brightness_ = optional(0.0f); - } else if (output_color) { - this->white_ = optional(0.0f); + // Cold/warm white value exists check + if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || + (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { + if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + this->cold_white_.reset(); + this->warm_white_.reset(); } } - // If only a color temperature is specified, change to white light - if (this->color_temperature_.has_value() && !this->white_.has_value() && !this->red_.has_value() && - !this->green_.has_value() && !this->blue_.has_value()) { - // Disable color LEDs explicitly if not already set - if (traits.get_supports_rgb() && !this->color_brightness_.has_value()) - this->color_brightness_ = optional(0.0f); - - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - - // if setting color temperature from color (i.e. switching to white light), set White to 100% - auto cv = this->parent_->remote_values; - bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; - if (traits.get_supports_color_interlock() || was_color) { - this->white_ = optional(1.0f); - } - } - -#define VALIDATE_RANGE_(name_, upper_name) \ +#define VALIDATE_RANGE_(name_, upper_name, min, max) \ if (name_##_.has_value()) { \ auto val = *name_##_; \ - if (val < 0.0f || val > 1.0f) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [0.0 - 1.0]!", name, upper_name, val); \ - name_##_ = clamp(val, 0.0f, 1.0f); \ + if (val < (min) || val > (max)) { \ + ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [%.1f - %.1f]!", name, upper_name, val, (min), (max)); \ + name_##_ = clamp(val, (min), (max)); \ } \ } -#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name) +#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) // Range checks VALIDATE_RANGE(brightness, "Brightness") @@ -292,13 +222,33 @@ LightColorValues LightCall::validate_() { VALIDATE_RANGE(green, "Green") VALIDATE_RANGE(blue, "Blue") VALIDATE_RANGE(white, "White") + VALIDATE_RANGE(cold_white, "Cold white") + VALIDATE_RANGE(warm_white, "Warm white") + VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) + // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. + bool explicit_turn_off_request = this->state_.has_value() && !*this->state_; + + // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). + if (this->brightness_.has_value() && *this->brightness_ == 0.0f) { + this->state_ = optional(false); + this->brightness_ = optional(1.0f); + } + + // Set color brightness to 100% if currently zero and a color is set. + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) + this->color_brightness_ = optional(1.0f); + } + + // Create color values for the light with this call applied. auto v = this->parent_->remote_values; + if (this->color_mode_.has_value()) + v.set_color_mode(*this->color_mode_); if (this->state_.has_value()) v.set_state(*this->state_); if (this->brightness_.has_value()) v.set_brightness(*this->brightness_); - if (this->color_brightness_.has_value()) v.set_color_brightness(*this->color_brightness_); if (this->red_.has_value()) @@ -309,11 +259,14 @@ LightColorValues LightCall::validate_() { v.set_blue(*this->blue_); if (this->white_.has_value()) v.set_white(*this->white_); - if (this->color_temperature_.has_value()) v.set_color_temperature(*this->color_temperature_); + if (this->cold_white_.has_value()) + v.set_cold_white(*this->cold_white_); + if (this->warm_white_.has_value()) + v.set_warm_white(*this->warm_white_); - v.normalize_color(traits); + v.normalize_color(); // Flash length check if (this->has_flash_() && *this->flash_length_ == 0) { @@ -322,7 +275,7 @@ LightColorValues LightCall::validate_() { } // validate transition length/flash length/effect not used at the same time - bool supports_transition = traits.get_supports_brightness(); + bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; // If effect is already active, remove effect start if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { @@ -331,7 +284,7 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' Invalid effect index %u", name, *this->effect_); + ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); this->effect_.reset(); } @@ -369,7 +322,7 @@ LightColorValues LightCall::validate_() { if (this->has_effect_()) { ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); this->effect_.reset(); - } else if (this->parent_->active_effect_index_ != 0) { + } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect this->effect_ = 0; } @@ -381,6 +334,127 @@ LightColorValues LightCall::validate_() { return v; } +void LightCall::transform_parameters_() { + auto traits = this->parent_->get_traits(); + + // Allow CWWW modes to be set with a white value and/or color temperature. This is used by HA, + // which doesn't support CWWW modes (yet?), and for compatibility with the pre-colormode model, + // as CWWW and RGBWW lights used to represent their values as white + color temperature. + if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // + (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // + !(*this->color_mode_ & ColorCapability::WHITE) && // + !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { + ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + this->parent_->get_name().c_str()); + auto current_values = this->parent_->remote_values; + if (this->color_temperature_.has_value()) { + const float white = + this->white_.value_or(fmaxf(current_values.get_cold_white(), current_values.get_warm_white())); + const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); + const float ww_fraction = + (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); + const float cw_fraction = 1.0f - ww_fraction; + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + this->cold_white_ = white * gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + this->warm_white_ = white * gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + } else { + const float max_cw_ww = std::max(current_values.get_warm_white(), current_values.get_cold_white()); + this->cold_white_ = *this->white_ * current_values.get_cold_white() / max_cw_ww; + this->warm_white_ = *this->white_ * current_values.get_warm_white() / max_cw_ww; + } + } +} +ColorMode LightCall::compute_color_mode_() { + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); + int supported_count = supported_modes.size(); + + // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. + if (supported_count == 0) + return ColorMode::UNKNOWN; + + // In the common case of lights supporting only a single mode, use that one. + if (supported_count == 1) + return *supported_modes.begin(); + + // Don't change if the light is being turned off. + ColorMode current_mode = this->parent_->remote_values.get_color_mode(); + if (this->state_.has_value() && !*this->state_) + return current_mode; + + // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to + // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode + // was used for some reason. + std::set suitable_modes = this->get_suitable_color_modes_(); + + // Don't change if the current mode is suitable. + if (suitable_modes.count(current_mode) > 0) { + ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", + this->parent_->get_name().c_str(), color_mode_to_human(current_mode)); + return current_mode; + } + + // Use the preferred suitable mode. + for (auto mode : suitable_modes) { + if (supported_modes.count(mode) == 0) + continue; + + ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + color_mode_to_human(mode)); + return mode; + } + + // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip + // out whatever we don't support. + auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); + ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", + this->parent_->get_name().c_str(), color_mode_to_human(color_mode)); + return color_mode; +} +std::set LightCall::get_suitable_color_modes_() { + bool has_white = this->white_.has_value() && *this->white_ > 0.0f; + bool has_ct = this->color_temperature_.has_value(); + bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || + (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); + bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || + (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + +#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) +#define ENTRY(white, ct, cwww, rgb, ...) \ + std::make_tuple>(KEY(white, ct, cwww, rgb), __VA_ARGS__) + + // Flag order: white, color temperature, cwww, rgb + std::array>, 10> lookup_table{ + ENTRY(true, false, false, false, + {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, false, + {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, false, + {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, false, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, + ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}), + ENTRY(true, false, false, true, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, true, + {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + }; + + auto key = KEY(has_white, has_ct, has_cwww, has_rgb); + for (auto &item : lookup_table) + if (std::get<0>(item) == key) + return std::get<1>(item); + + // This happens if there are conflicting flags given. + return {}; +} + LightCall &LightCall::set_effect(const std::string &effect) { if (strcasecmp(effect.c_str(), "none") == 0) { this->set_effect(0); @@ -406,53 +480,74 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) { this->set_state(values.is_on()); this->set_brightness_if_supported(values.get_brightness()); this->set_color_brightness_if_supported(values.get_color_brightness()); + this->set_color_mode_if_supported(values.get_color_mode()); this->set_red_if_supported(values.get_red()); this->set_green_if_supported(values.get_green()); this->set_blue_if_supported(values.get_blue()); this->set_white_if_supported(values.get_white()); this->set_color_temperature_if_supported(values.get_color_temperature()); + this->set_cold_white_if_supported(values.get_cold_white()); + this->set_warm_white_if_supported(values.get_warm_white()); return *this; } +ColorMode LightCall::get_active_color_mode_() { + return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); +} LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { - if (this->parent_->get_traits().get_supports_brightness()) + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) this->set_transition_length(transition_length); return *this; } LightCall &LightCall::set_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_brightness()) + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) this->set_brightness(brightness); return *this; } +LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { + if (this->parent_->get_traits().supports_color_mode(color_mode)) + this->color_mode_ = color_mode; + return *this; +} LightCall &LightCall::set_color_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_color_brightness(brightness); return *this; } LightCall &LightCall::set_red_if_supported(float red) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_red(red); return *this; } LightCall &LightCall::set_green_if_supported(float green) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_green(green); return *this; } LightCall &LightCall::set_blue_if_supported(float blue) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_blue(blue); return *this; } LightCall &LightCall::set_white_if_supported(float white) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) + if (this->get_active_color_mode_() & ColorCapability::WHITE) this->set_white(white); return *this; } LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { - if (this->parent_->get_traits().get_supports_color_temperature()) + if (this->get_active_color_mode_() & ColorCapability::COLOR_TEMPERATURE) this->set_color_temperature(color_temperature); return *this; } +LightCall &LightCall::set_cold_white_if_supported(float cold_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_cold_white(cold_white); + return *this; +} +LightCall &LightCall::set_warm_white_if_supported(float warm_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_warm_white(warm_white); + return *this; +} LightCall &LightCall::set_state(optional state) { this->state_ = state; return *this; @@ -485,6 +580,14 @@ LightCall &LightCall::set_brightness(float brightness) { this->brightness_ = brightness; return *this; } +LightCall &LightCall::set_color_mode(optional color_mode) { + this->color_mode_ = color_mode; + return *this; +} +LightCall &LightCall::set_color_mode(ColorMode color_mode) { + this->color_mode_ = color_mode; + return *this; +} LightCall &LightCall::set_color_brightness(optional brightness) { this->color_brightness_ = brightness; return *this; @@ -533,6 +636,22 @@ LightCall &LightCall::set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; return *this; } +LightCall &LightCall::set_cold_white(optional cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_cold_white(float cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_warm_white(optional warm_white) { + this->warm_white_ = warm_white; + return *this; +} +LightCall &LightCall::set_warm_white(float warm_white) { + this->warm_white_ = warm_white; + return *this; +} LightCall &LightCall::set_effect(optional effect) { if (effect.has_value()) this->set_effect(*effect); diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index c63b63bc54..bca2ac7b07 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -2,6 +2,7 @@ #include "esphome/core/optional.h" #include "light_color_values.h" +#include namespace esphome { namespace light { @@ -44,6 +45,14 @@ class LightCall { LightCall &set_brightness(float brightness); /// Set the brightness property if the light supports brightness. LightCall &set_brightness_if_supported(float brightness); + + /// Set the color mode of the light. + LightCall &set_color_mode(optional color_mode); + /// Set the color mode of the light. + LightCall &set_color_mode(ColorMode color_mode); + /// Set the color mode of the light, if this mode is supported. + LightCall &set_color_mode_if_supported(ColorMode color_mode); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) LightCall &set_color_brightness(optional brightness); /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) @@ -98,6 +107,18 @@ class LightCall { LightCall &set_color_temperature(float color_temperature); /// Set the color_temperature property if the light supports color temperature. LightCall &set_color_temperature_if_supported(float color_temperature); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(optional cold_white); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(float cold_white); + /// Set the cold white property if the light supports cold white output. + LightCall &set_cold_white_if_supported(float cold_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(optional warm_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(float warm_white); + /// Set the warm white property if the light supports cold white output. + LightCall &set_warm_white_if_supported(float warm_white); /// Set the effect of the light by its name. LightCall &set_effect(optional effect); /// Set the effect of the light by its name. @@ -131,18 +152,24 @@ class LightCall { * @return The light call for chaining setters. */ LightCall &set_rgbw(float red, float green, float blue, float white); -#ifdef USE_JSON - LightCall &parse_color_json(JsonObject &root); - LightCall &parse_json(JsonObject &root); -#endif LightCall &from_light_color_values(const LightColorValues &values); void perform(); protected: + /// Get the currently targeted, or active if none set, color mode. + ColorMode get_active_color_mode_(); + /// Validate all properties and return the target light color values. LightColorValues validate_(); + //// Compute the color mode that should be used for this call. + ColorMode compute_color_mode_(); + /// Get potential color modes for this light call. + std::set get_suitable_color_modes_(); + /// Some color modes also can be set using non-native parameters, transform those calls. + void transform_parameters_(); + bool has_transition_() { return this->transition_length_.has_value(); } bool has_flash_() { return this->flash_length_.has_value(); } bool has_effect_() { return this->effect_.has_value(); } @@ -151,6 +178,7 @@ class LightCall { optional state_; optional transition_length_; optional flash_length_; + optional color_mode_; optional brightness_; optional color_brightness_; optional red_; @@ -158,6 +186,8 @@ class LightCall { optional blue_; optional white_; optional color_temperature_; + optional cold_white_; + optional warm_white_; optional effect_; bool publish_{true}; bool save_{true}; diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 54dcaea5a3..dd74c396c6 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -1,52 +1,65 @@ #pragma once #include "esphome/core/helpers.h" -#include "esphome/core/defines.h" -#include "light_traits.h" - -#ifdef USE_JSON -#include "esphome/components/json/json_util.h" -#endif +#include "color_mode.h" namespace esphome { namespace light { +inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } + /** This class represents the color state for a light object. * - * All values in this class (except color temperature) are represented using floats in the range - * from 0.0 (off) to 1.0 (on). Please note that all values are automatically clamped to this range. + * The representation of the color state is dependent on the active color mode. A color mode consists of multiple + * color capabilities, and each color capability has its own representation in this class. The fields available are as + * follows: * - * This class has the following properties: - * - state: Whether the light should be on/off. Represented as a float for transitions. Used for - * all lights. - * - brightness: The master brightness of the light, applied to all channels. Used for all lights - * with brightness control. - * - color_brightness: The brightness of the color channels of the light. Used for RGB, RGBW and - * RGBWW lights. - * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of - * them is always 1.0. - * - white: The brightness of the white channel of the light. Used for RGBW and RGBWW lights. - * - color_temperature: The color temperature of the white channel in mireds. Used for RGBWW and - * CWWW lights. + * Always: + * - color_mode: The currently active color mode. * - * For lights with a color interlock (RGB lights and white light cannot be on at the same time), a - * valid state has always either color_brightness or white (or both) set to zero. + * For ON_OFF capability: + * - state: Whether the light should be on/off. Represented as a float for transitions. + * + * For BRIGHTNESS capability: + * - brightness: The master brightness of the light, should be applied to all channels. + * + * For RGB capability: + * - color_brightness: The brightness of the color channels of the light. + * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of them is always 1.0. + * + * For WHITE capability: + * - white: The brightness of the white channel of the light. + * + * For COLOR_TEMPERATURE capability: + * - color_temperature: The color temperature of the white channel in mireds. Note that it is not clamped to the valid + * range as set in the traits, so the output needs to do this. + * + * For COLD_WARM_WHITE capability: + * - cold_white, warm_white: The brightness of the cald and warm white channels of the light. + * + * All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are + * automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values + * and must not be accessed by the light output. */ class LightColorValues { public: - /// Construct the LightColorValues with all attributes enabled, but state set to 0.0 + /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : state_(0.0f), + : color_mode_(ColorMode::UNKNOWN), + state_(0.0f), brightness_(1.0f), color_brightness_(1.0f), red_(1.0f), green_(1.0f), blue_(1.0f), white_(1.0f), - color_temperature_{1.0f} {} + color_temperature_{0.0f}, + cold_white_{1.0f}, + warm_white_{1.0f} {} - LightColorValues(float state, float brightness, float color_brightness, float red, float green, float blue, - float white, float color_temperature = 1.0f) { + LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, + float blue, float white, float color_temperature, float cold_white, float warm_white) { + this->set_color_mode(color_mode); this->set_state(state); this->set_brightness(brightness); this->set_color_brightness(color_brightness); @@ -55,49 +68,8 @@ class LightColorValues { this->set_blue(blue); this->set_white(white); this->set_color_temperature(color_temperature); - } - - LightColorValues(bool state, float brightness, float color_brightness, float red, float green, float blue, - float white, float color_temperature = 1.0f) - : LightColorValues(state ? 1.0f : 0.0f, brightness, color_brightness, red, green, blue, white, - color_temperature) {} - - /// Create light color values from a binary true/false state. - static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } - - /// Create light color values from a monochromatic brightness state. - static LightColorValues from_monochromatic(float brightness) { - if (brightness == 0.0f) - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - else - return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } - - /// Create light color values from an RGB state. - static LightColorValues from_rgb(float r, float g, float b) { - float brightness = std::max(r, std::max(g, b)); - if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } else { - return {1.0f, brightness, 1.0f, r / brightness, g / brightness, b / brightness, 1.0f}; - } - } - - /// Create light color values from an RGBW state. - static LightColorValues from_rgbw(float r, float g, float b, float w) { - float color_brightness = std::max(r, std::max(g, b)); - float master_brightness = std::max(color_brightness, w); - if (master_brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } else { - return {1.0f, - master_brightness, - color_brightness / master_brightness, - r / color_brightness, - g / color_brightness, - b / color_brightness, - w / master_brightness}; - } + this->set_cold_white(cold_white); + this->set_warm_white(warm_white); } /** Linearly interpolate between the values in start to the values in end. @@ -112,6 +84,7 @@ class LightColorValues { */ static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; + v.set_color_mode(end.color_mode_); v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); @@ -120,33 +93,11 @@ class LightColorValues { v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); + v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); + v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); return v; } -#ifdef USE_JSON - /** Dump this color into a JsonObject. Only dumps values if the corresponding traits are marked supported by traits. - * - * @param root The json root object. - * @param traits The traits object used for determining whether to include certain attributes. - */ - void dump_json(JsonObject &root, const LightTraits &traits) const { - root["state"] = (this->get_state() != 0.0f) ? "ON" : "OFF"; - if (traits.get_supports_brightness()) - root["brightness"] = uint8_t(this->get_brightness() * 255); - if (traits.get_supports_rgb()) { - JsonObject &color = root.createNestedObject("color"); - color["r"] = uint8_t(this->get_color_brightness() * this->get_red() * 255); - color["g"] = uint8_t(this->get_color_brightness() * this->get_green() * 255); - color["b"] = uint8_t(this->get_color_brightness() * this->get_blue() * 255); - } - if (traits.get_supports_rgb_white_value()) { - root["white_value"] = uint8_t(this->get_white() * 255); - } - if (traits.get_supports_color_temperature()) - root["color_temp"] = uint32_t(this->get_color_temperature()); - } -#endif - /** Normalize the color (RGB/W) component. * * Divides all color attributes by the maximum attribute, so effectively set at least one attribute to 1. @@ -156,8 +107,8 @@ class LightColorValues { * * @param traits Used for determining which attributes to consider. */ - void normalize_color(const LightTraits &traits) { - if (traits.get_supports_rgb()) { + void normalize_color() { + if (this->color_mode_ & ColorCapability::RGB) { float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); if (max_value == 0.0f) { this->set_red(1.0f); @@ -169,15 +120,11 @@ class LightColorValues { this->set_blue(this->get_blue() / max_value); } } - - if (traits.get_supports_brightness() && this->get_brightness() == 0.0f) { - // 0% brightness means off - this->set_state(false); - // reset brightness to 100% - this->set_brightness(1.0f); - } } + // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters + // are always used or necessary. Methods will be deprecated later. + /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } @@ -188,64 +135,92 @@ class LightColorValues { /// Convert these light color values to an RGB representation and write them to red, green, blue. void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { - float brightness = this->state_ * this->brightness_ * this->color_brightness_; - if (color_interlock && this->white_ > 0.0f) { - brightness = 0; + if (this->color_mode_ & ColorCapability::RGB) { + float brightness = this->state_ * this->brightness_ * this->color_brightness_; + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); + } else { + *red = *green = *blue = 0; } - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - } - - /// Convert these light color values to an RGBWW representation with the given parameters. - void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, - bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; + this->as_rgb(red, green, blue, gamma); + if (this->color_mode_ & ColorCapability::WHITE) { + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + } else { + *white = 0; } } + /// Convert these light color values to an RGBWW representation with the given parameters. + void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, + bool constant_brightness = false) const { + this->as_rgb(red, green, blue, gamma); + this->as_cwww(cold_white, warm_white, gamma, constant_brightness); + } + + /// Convert these light color values to an RGB+CT+BR representation with the given parameters. + void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, + float *color_temperature, float *white_brightness, float gamma = 0) const { + this->as_rgb(red, green, blue, gamma); + this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma); + } + /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, - float gamma = 0, bool constant_brightness = false) const { - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; + void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { + if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { + const float cw_level = gamma_correct(this->cold_white_, gamma); + const float ww_level = gamma_correct(this->warm_white_, gamma); + const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + if (!constant_brightness) { + *cold_white = white_level * cw_level; + *warm_white = white_level * ww_level; + } else { + // Just multiplying by cw_level / (cw_level + ww_level) would divide out the brightness information from the + // cold_white and warm_white settings (i.e. cw=0.8, ww=0.4 would be identical to cw=0.4, ww=0.2), which breaks + // transitions. Use the highest value as the brightness for the white channels (the alternative, using cw+ww/2, + // reduces to cw/2 and ww/2, which would still limit brightness to 100% of a single channel, but isn't very + // useful in all other aspects -- that behaviour can also be achieved by limiting the output power). + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; + } + } else { + *cold_white = *warm_white = 0; + } + } + + /// Convert these light color values to a CT+BR representation with the given parameters. + void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness, + float gamma = 0) const { + const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1; + if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) { + *color_temperature = + (this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); + *white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma); + } else { // Probably wont get here but put this here anyway. + *white_brightness = 0; } } /// Compare this LightColorValues to rhs, return true if and only if all attributes match. bool operator==(const LightColorValues &rhs) const { - return state_ == rhs.state_ && brightness_ == rhs.brightness_ && color_brightness_ == rhs.color_brightness_ && - red_ == rhs.red_ && green_ == rhs.green_ && blue_ == rhs.blue_ && white_ == rhs.white_ && - color_temperature_ == rhs.color_temperature_; + return color_mode_ == rhs.color_mode_ && state_ == rhs.state_ && brightness_ == rhs.brightness_ && + color_brightness_ == rhs.color_brightness_ && red_ == rhs.red_ && green_ == rhs.green_ && + blue_ == rhs.blue_ && white_ == rhs.white_ && color_temperature_ == rhs.color_temperature_ && + cold_white_ == rhs.cold_white_ && warm_white_ == rhs.warm_white_; } bool operator!=(const LightColorValues &rhs) const { return !(rhs == *this); } + /// Get the color mode of these light color values. + ColorMode get_color_mode() const { return this->color_mode_; } + /// Set the color mode of these light color values. + void set_color_mode(ColorMode color_mode) { this->color_mode_ = color_mode; } + /// Get the state of these light color values. In range from 0.0 (off) to 1.0 (on) float get_state() const { return this->state_; } /// Get the binary true/false state of these light color values. @@ -288,11 +263,20 @@ class LightColorValues { /// Get the color temperature property of these light color values in mired. float get_color_temperature() const { return this->color_temperature_; } /// Set the color temperature property of these light color values in mired. - void set_color_temperature(float color_temperature) { - this->color_temperature_ = std::max(0.000001f, color_temperature); - } + void set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; } + + /// Get the cold white property of these light color values. In range 0.0 to 1.0. + float get_cold_white() const { return this->cold_white_; } + /// Set the cold white property of these light color values. In range 0.0 to 1.0. + void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); } + + /// Get the warm white property of these light color values. In range 0.0 to 1.0. + float get_warm_white() const { return this->warm_white_; } + /// Set the warm white property of these light color values. In range 0.0 to 1.0. + void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } protected: + ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -301,6 +285,8 @@ class LightColorValues { float blue_; float white_; float color_temperature_; ///< Color Temperature in Mired + float cold_white_; + float warm_white_; }; } // namespace light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index f9903397b4..8da51fe8b3 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -3,8 +3,6 @@ #include #include "esphome/core/component.h" -#include "light_color_values.h" -#include "light_state.h" namespace esphome { namespace light { diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp new file mode 100644 index 0000000000..2e07d91046 --- /dev/null +++ b/esphome/components/light/light_json_schema.cpp @@ -0,0 +1,165 @@ +#include "light_json_schema.h" +#include "light_output.h" + +#ifdef USE_JSON + +namespace esphome { +namespace light { + +// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema + +void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { + if (state.supports_effects()) + root["effect"] = state.get_effect_name(); + + auto values = state.remote_values; + auto traits = state.get_output()->get_traits(); + + switch (values.get_color_mode()) { + case ColorMode::UNKNOWN: // don't need to set color mode if we don't know it + break; + case ColorMode::ON_OFF: + root["color_mode"] = "onoff"; + break; + case ColorMode::BRIGHTNESS: + root["color_mode"] = "brightness"; + break; + case ColorMode::WHITE: // not supported by HA in MQTT + root["color_mode"] = "white"; + break; + case ColorMode::COLOR_TEMPERATURE: + root["color_mode"] = "color_temp"; + break; + case ColorMode::COLD_WARM_WHITE: // not supported by HA + root["color_mode"] = "cwww"; + break; + case ColorMode::RGB: + root["color_mode"] = "rgb"; + break; + case ColorMode::RGB_WHITE: + root["color_mode"] = "rgbw"; + break; + case ColorMode::RGB_COLOR_TEMPERATURE: // not supported by HA + root["color_mode"] = "rgbct"; + break; + case ColorMode::RGB_COLD_WARM_WHITE: + root["color_mode"] = "rgbww"; + break; + } + + if (values.get_color_mode() & ColorCapability::ON_OFF) + root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF"; + if (values.get_color_mode() & ColorCapability::BRIGHTNESS) + root["brightness"] = uint8_t(values.get_brightness() * 255); + + JsonObject &color = root.createNestedObject("color"); + if (values.get_color_mode() & ColorCapability::RGB) { + color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); + color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); + color["b"] = uint8_t(values.get_color_brightness() * values.get_blue() * 255); + } + if (values.get_color_mode() & ColorCapability::WHITE) { + color["w"] = uint8_t(values.get_white() * 255); + root["white_value"] = uint8_t(values.get_white() * 255); // legacy API + } + if (values.get_color_mode() & ColorCapability::COLOR_TEMPERATURE) { + // this one isn't under the color subkey for some reason + root["color_temp"] = uint32_t(values.get_color_temperature()); + } + if (values.get_color_mode() & ColorCapability::COLD_WARM_WHITE) { + color["c"] = uint8_t(values.get_cold_white() * 255); + color["w"] = uint8_t(values.get_warm_white() * 255); + } +} + +void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject &root) { + if (root.containsKey("state")) { + auto val = parse_on_off(root["state"]); + switch (val) { + case PARSE_ON: + call.set_state(true); + break; + case PARSE_OFF: + call.set_state(false); + break; + case PARSE_TOGGLE: + call.set_state(!state.remote_values.is_on()); + break; + case PARSE_NONE: + break; + } + } + + if (root.containsKey("brightness")) { + call.set_brightness(float(root["brightness"]) / 255.0f); + } + + if (root.containsKey("color")) { + JsonObject &color = root["color"]; + // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. + float max_rgb = 0.0f; + if (color.containsKey("r")) { + float r = float(color["r"]) / 255.0f; + max_rgb = fmaxf(max_rgb, r); + call.set_red(r); + } + if (color.containsKey("g")) { + float g = float(color["g"]) / 255.0f; + max_rgb = fmaxf(max_rgb, g); + call.set_green(g); + } + if (color.containsKey("b")) { + float b = float(color["b"]) / 255.0f; + max_rgb = fmaxf(max_rgb, b); + call.set_blue(b); + } + if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + call.set_color_brightness(max_rgb); + } + + if (color.containsKey("c")) { + call.set_cold_white(float(color["c"]) / 255.0f); + } + if (color.containsKey("w")) { + // the HA scheme is ambigious here, the same key is used for white channel in RGBW and warm + // white channel in RGBWW. + if (color.containsKey("c")) { + call.set_warm_white(float(color["w"]) / 255.0f); + } else { + call.set_white(float(color["w"]) / 255.0f); + } + } + } + + if (root.containsKey("white_value")) { // legacy API + call.set_white(float(root["white_value"]) / 255.0f); + } + + if (root.containsKey("color_temp")) { + call.set_color_temperature(float(root["color_temp"])); + } +} + +void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject &root) { + LightJSONSchema::parse_color_json(state, call, root); + + if (root.containsKey("flash")) { + auto length = uint32_t(float(root["flash"]) * 1000); + call.set_flash_length(length); + } + + if (root.containsKey("transition")) { + auto length = uint32_t(float(root["transition"]) * 1000); + call.set_transition_length(length); + } + + if (root.containsKey("effect")) { + const char *effect = root["effect"]; + call.set_effect(effect); + } +} + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h new file mode 100644 index 0000000000..09a372f11c --- /dev/null +++ b/esphome/components/light/light_json_schema.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_JSON + +#include "esphome/components/json/json_util.h" +#include "light_call.h" +#include "light_state.h" + +namespace esphome { +namespace light { + +class LightJSONSchema { + public: + /// Dump the state of a light as JSON. + static void dump_json(LightState &state, JsonObject &root); + /// Parse the JSON state of a light to a LightCall. + static void parse_json(LightState &state, LightCall &call, JsonObject &root); + + protected: + static void parse_color_json(LightState &state, LightCall &call, JsonObject &root); +}; + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp new file mode 100644 index 0000000000..e805a0b694 --- /dev/null +++ b/esphome/components/light/light_output.cpp @@ -0,0 +1,12 @@ +#include "light_output.h" +#include "transformers.h" + +namespace esphome { +namespace light { + +std::unique_ptr LightOutput::create_default_transition() { + return make_unique(); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 9e47092b0f..7568ea6831 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -3,18 +3,20 @@ #include "esphome/core/component.h" #include "light_traits.h" #include "light_state.h" +#include "light_transformer.h" namespace esphome { namespace light { -class LightState; - /// Interface to write LightStates to hardware. class LightOutput { public: /// Return the LightTraits of this LightOutput. virtual LightTraits get_traits() = 0; + /// Return the default transformer used for transitions. + virtual std::unique_ptr create_default_transition(); + virtual void setup_state(LightState *state) {} virtual void write_state(LightState *state) = 0; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index e32b15daf1..278229fbd1 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,6 +1,7 @@ +#include "esphome/core/log.h" #include "light_state.h" #include "light_output.h" -#include "esphome/core/log.h" +#include "transformers.h" namespace esphome { namespace light { @@ -16,6 +17,7 @@ LightCall LightState::toggle() { return this->make_call().set_state(!this->remot LightCall LightState::make_call() { return LightCall(this); } struct LightStateRTCState { + ColorMode color_mode{ColorMode::UNKNOWN}; bool state{false}; float brightness{1.0f}; float color_brightness{1.0f}; @@ -24,6 +26,8 @@ struct LightStateRTCState { float blue{1.0f}; float white{1.0f}; float color_temp{1.0f}; + float cold_white{1.0f}; + float warm_white{1.0f}; uint32_t effect{0}; }; @@ -64,6 +68,7 @@ void LightState::setup() { break; } + call.set_color_mode_if_supported(recovered.color_mode); call.set_state(recovered.state); call.set_brightness_if_supported(recovered.brightness); call.set_color_brightness_if_supported(recovered.color_brightness); @@ -72,6 +77,8 @@ void LightState::setup() { call.set_blue_if_supported(recovered.blue); call.set_white_if_supported(recovered.white); call.set_color_temperature_if_supported(recovered.color_temp); + call.set_cold_white_if_supported(recovered.cold_white); + call.set_warm_white_if_supported(recovered.warm_white); if (recovered.effect != 0) { call.set_effect(recovered.effect); } else { @@ -81,11 +88,11 @@ void LightState::setup() { } void LightState::dump_config() { ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); - if (this->get_traits().get_supports_brightness()) { + if (this->get_traits().supports_color_capability(ColorCapability::BRIGHTNESS)) { ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); } - if (this->get_traits().get_supports_color_temperature()) { + if (this->get_traits().supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) { ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); } @@ -99,19 +106,19 @@ void LightState::loop() { // Apply transformer (if any) if (this->transformer_ != nullptr) { + auto values = this->transformer_->apply(); + this->next_write_ = values.has_value(); // don't write if transformer doesn't want us to + if (values.has_value()) + this->current_values = *values; + if (this->transformer_->is_finished()) { - this->remote_values = this->current_values = this->transformer_->get_end_values(); - this->target_state_reached_callback_.call(); - if (this->transformer_->publish_at_end()) - this->publish_state(); + this->transformer_->stop(); this->transformer_ = nullptr; - } else { - this->current_values = this->transformer_->get_values(); - this->remote_values = this->transformer_->get_remote_values(); + this->target_state_reached_callback_.call(); } - this->next_write_ = true; } + // Write state to the light if (this->next_write_) { this->output_->write_state(this); this->next_write_ = false; @@ -141,14 +148,6 @@ void LightState::add_new_target_state_reached_callback(std::function &&s this->target_state_reached_callback_.add(std::move(send_callback)); } -#ifdef USE_JSON -void LightState::dump_json(JsonObject &root) { - if (this->supports_effects()) - root["effect"] = this->get_effect_name(); - this->remote_values.dump_json(root, this->output_->get_traits()); -} -#endif - void LightState::set_default_transition_length(uint32_t default_transition_length) { this->default_transition_length_ = default_transition_length; } @@ -169,23 +168,30 @@ void LightState::current_values_as_brightness(float *brightness) { } void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { auto traits = this->get_traits(); - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); } void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { auto traits = this->get_traits(); - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness, bool color_interlock) { + bool constant_brightness) { + this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness); +} +void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, + float *white_brightness) { auto traits = this->get_traits(); - this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, this->gamma_correct_, constant_brightness, - traits.get_supports_color_interlock()); + this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature, + white_brightness, this->gamma_correct_); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); - this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, - this->gamma_correct_, constant_brightness); + this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); +} +void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { + auto traits = this->get_traits(); + this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness, + this->gamma_correct_); } void LightState::start_effect_(uint32_t effect_index) { @@ -212,18 +218,21 @@ void LightState::stop_effect_() { } void LightState::start_transition_(const LightColorValues &target, uint32_t length) { - this->transformer_ = make_unique(millis(), length, this->current_values, target); - this->remote_values = this->transformer_->get_remote_values(); + this->transformer_ = this->output_->create_default_transition(); + this->transformer_->setup(this->current_values, target, length); + this->remote_values = target; } void LightState::start_flash_(const LightColorValues &target, uint32_t length) { - LightColorValues end_colors = this->current_values; + LightColorValues end_colors = this->remote_values; // If starting a flash if one is already happening, set end values to end values of current flash // Hacky but works if (this->transformer_ != nullptr) - end_colors = this->transformer_->get_end_values(); - this->transformer_ = make_unique(millis(), length, end_colors, target); - this->remote_values = this->transformer_->get_remote_values(); + end_colors = this->transformer_->get_target_values(); + + this->transformer_ = make_unique(*this); + this->transformer_->setup(end_colors, target, length); + this->remote_values = target; } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -235,12 +244,9 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot this->next_write_ = true; } -void LightState::set_transformer_(std::unique_ptr transformer) { - this->transformer_ = std::move(transformer); -} - void LightState::save_remote_values_() { LightStateRTCState saved; + saved.color_mode = this->remote_values.get_color_mode(); saved.state = this->remote_values.is_on(); saved.brightness = this->remote_values.get_brightness(); saved.color_brightness = this->remote_values.get_color_brightness(); @@ -249,6 +255,8 @@ void LightState::save_remote_values_() { saved.blue = this->remote_values.get_blue(); saved.white = this->remote_values.get_white(); saved.color_temp = this->remote_values.get_color_temperature(); + saved.cold_white = this->remote_values.get_cold_white(); + saved.warm_white = this->remote_values.get_warm_white(); saved.effect = this->active_effect_index_; this->rtc_.save(&saved); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index cd5e6ad1cb..dfea9a15f4 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -3,9 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/optional.h" #include "esphome/core/preferences.h" -#include "light_effect.h" -#include "light_color_values.h" #include "light_call.h" +#include "light_color_values.h" +#include "light_effect.h" #include "light_traits.h" #include "light_transformer.h" @@ -54,6 +54,8 @@ class LightState : public Nameable, public Component { * property will be changed continuously (in contrast to .remote_values, where they * are constant during transitions). * + * This value does not have gamma correction applied. + * * This property is read-only for users. Any changes to it will be ignored. */ LightColorValues current_values; @@ -64,6 +66,8 @@ class LightState : public Nameable, public Component { * continuously change the "current" values. But the remote values will immediately * switch to the target value for a transition, reducing the number of packets sent. * + * This value does not have gamma correction applied. + * * This property is read-only for users. Any changes to it will be ignored. */ LightColorValues remote_values; @@ -93,11 +97,6 @@ class LightState : public Nameable, public Component { */ void add_new_target_state_reached_callback(std::function &&send_callback); -#ifdef USE_JSON - /// Dump the state of this light as JSON. - void dump_json(JsonObject &root); -#endif - /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); @@ -117,6 +116,7 @@ class LightState : public Nameable, public Component { /// Add effects for this light state. void add_effects(const std::vector &effects); + /// The result of all the current_values_as_* methods have gamma correction applied. void current_values_as_binary(bool *binary); void current_values_as_brightness(float *brightness); @@ -126,10 +126,15 @@ class LightState : public Nameable, public Component { void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness = false, bool color_interlock = false); + bool constant_brightness = false); + + void current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, + float *white_brightness); void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); + void current_values_as_ct(float *color_temperature, float *white_brightness); + protected: friend LightOutput; friend LightCall; @@ -152,9 +157,6 @@ class LightState : public Nameable, public Component { /// Internal method to set the color values to target immediately (with no transition). void set_immediately_(const LightColorValues &target, bool set_remote_values); - /// Internal method to start a transformer. - void set_transformer_(std::unique_ptr transformer); - /// Internal method to save the current remote_values to the preferences void save_remote_values_(); diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index ed9c0d44ea..7c99d721f0 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -1,5 +1,9 @@ #pragma once +#include "esphome/core/helpers.h" +#include "color_mode.h" +#include + namespace esphome { namespace light { @@ -8,35 +12,49 @@ class LightTraits { public: LightTraits() = default; - bool get_supports_brightness() const { return this->supports_brightness_; } - void set_supports_brightness(bool supports_brightness) { this->supports_brightness_ = supports_brightness; } - bool get_supports_rgb() const { return this->supports_rgb_; } - void set_supports_rgb(bool supports_rgb) { this->supports_rgb_ = supports_rgb; } - bool get_supports_rgb_white_value() const { return this->supports_rgb_white_value_; } - void set_supports_rgb_white_value(bool supports_rgb_white_value) { - this->supports_rgb_white_value_ = supports_rgb_white_value; + const std::set &get_supported_color_modes() const { return this->supported_color_modes_; } + void set_supported_color_modes(std::set supported_color_modes) { + this->supported_color_modes_ = std::move(supported_color_modes); } - bool get_supports_color_temperature() const { return this->supports_color_temperature_; } - void set_supports_color_temperature(bool supports_color_temperature) { - this->supports_color_temperature_ = supports_color_temperature; + + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); } + bool supports_color_capability(ColorCapability color_capability) const { + for (auto mode : this->supported_color_modes_) { + if (mode & color_capability) + return true; + } + return false; } - bool get_supports_color_interlock() const { return this->supports_color_interlock_; } - void set_supports_color_interlock(bool supports_color_interlock) { - this->supports_color_interlock_ = supports_color_interlock; + + ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21") + bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); } + ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21") + bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); } + ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21") + bool get_supports_rgb_white_value() const { + return this->supports_color_mode(ColorMode::RGB_WHITE) || + this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE); } + ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21") + bool get_supports_color_temperature() const { + return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE); + } + ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21") + bool get_supports_color_interlock() const { + return this->supports_color_mode(ColorMode::RGB) && + (this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) || + this->supports_color_mode(ColorMode::COLOR_TEMPERATURE)); + } + float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: - bool supports_brightness_{false}; - bool supports_rgb_{false}; - bool supports_rgb_white_value_{false}; - bool supports_color_temperature_{false}; + std::set supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; - bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 222be7802c..c5181abd4f 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -1,42 +1,42 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "light_color_values.h" namespace esphome { namespace light { -/// Base-class for all light color transformers, such as transitions or flashes. +/// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { public: - LightTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values, - const LightColorValues &target_values) - : start_time_(start_time), length_(length), start_values_(start_values), target_values_(target_values) {} + void setup(const LightColorValues &start_values, const LightColorValues &target_values, uint32_t length) { + this->start_time_ = millis(); + this->length_ = length; + this->start_values_ = start_values; + this->target_values_ = target_values; + this->start(); + } - LightTransformer() = delete; + /// Indicates whether this transformation is finished. + virtual bool is_finished() { return this->get_progress_() >= 1.0f; } - /// Whether this transformation is finished - virtual bool is_finished() { return this->get_progress() >= 1.0f; } + /// This will be called before the transition is started. + virtual void start() {} - /// This will be called to get the current values for output. - virtual LightColorValues get_values() = 0; + /// This will be called while the transformer is active to apply the transition to the light. Can either write to the + /// light directly, or return LightColorValues that will be applied. + virtual optional apply() = 0; - /// The values that should be reported to the front-end. - virtual LightColorValues get_remote_values() { return this->get_target_values_(); } + /// This will be called after transition is finished. + virtual void stop() {} - /// The values that should be set after this transformation is complete. - virtual LightColorValues get_end_values() { return this->get_target_values_(); } + const LightColorValues &get_start_values() const { return this->start_values_; } - virtual bool publish_at_end() = 0; - virtual bool is_transition() = 0; - - float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } + const LightColorValues &get_target_values() const { return this->target_values_; } protected: - const LightColorValues &get_start_values_() const { return this->start_values_; } - - const LightColorValues &get_target_values_() const { return this->target_values_; } + /// The progress of this transition, on a scale of 0 to 1. + float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } uint32_t start_time_; uint32_t length_; @@ -44,46 +44,5 @@ class LightTransformer { LightColorValues target_values_; }; -class LightTransitionTransformer : public LightTransformer { - public: - LightTransitionTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values, - const LightColorValues &target_values) - : LightTransformer(start_time, length, start_values, target_values) { - // When turning light on from off state, use colors from new. - if (!this->start_values_.is_on() && this->target_values_.is_on()) { - this->start_values_.set_brightness(0.0f); - this->start_values_.set_red(target_values.get_red()); - this->start_values_.set_green(target_values.get_green()); - this->start_values_.set_blue(target_values.get_blue()); - this->start_values_.set_white(target_values.get_white()); - this->start_values_.set_color_temperature(target_values.get_color_temperature()); - } - } - - LightColorValues get_values() override { - float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); - return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); - } - - bool publish_at_end() override { return false; } - bool is_transition() override { return true; } - - static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } -}; - -class LightFlashTransformer : public LightTransformer { - public: - LightFlashTransformer(uint32_t start_time, uint32_t length, const LightColorValues &start_values, - const LightColorValues &target_values) - : LightTransformer(start_time, length, start_values, target_values) {} - - LightColorValues get_values() override { return this->get_target_values_(); } - - LightColorValues get_end_values() override { return this->get_start_values_(); } - - bool publish_at_end() override { return true; } - bool is_transition() override { return false; } -}; - } // namespace light } // namespace esphome diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h new file mode 100644 index 0000000000..fd0bfd20f3 --- /dev/null +++ b/esphome/components/light/transformers.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "light_color_values.h" +#include "light_state.h" +#include "light_transformer.h" + +namespace esphome { +namespace light { + +class LightTransitionTransformer : public LightTransformer { + public: + void start() override { + // When turning light on from off state, use colors from target state. + if (!this->start_values_.is_on() && this->target_values_.is_on()) { + this->start_values_ = LightColorValues(this->target_values_); + this->start_values_.set_brightness(0.0f); + } + + // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. + if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->changing_color_mode_ = true; + this->intermediate_values_ = this->start_values_; + this->intermediate_values_.set_state(false); + } + } + + optional apply() override { + float p = this->get_progress_(); + + // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. + if (this->changing_color_mode_ && p > 0.5f && + this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->intermediate_values_ = this->target_values_; + this->intermediate_values_.set_state(false); + } + + LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; + LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_; + if (this->changing_color_mode_) + p = p < 0.5f ? p * 2 : (p - 0.5) * 2; + + float v = LightTransitionTransformer::smoothed_progress(p); + return LightColorValues::lerp(start, end, v); + } + + protected: + // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like + // transition from 0 to 1 on x = [0, 1] + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } + + bool changing_color_mode_{false}; + LightColorValues intermediate_values_{}; +}; + +class LightFlashTransformer : public LightTransformer { + public: + LightFlashTransformer(LightState &state) : state_(state) {} + + optional apply() override { return this->get_target_values(); } + + // Restore the original values after the flash. + void stop() override { + this->state_.current_values = this->get_start_values(); + this->state_.remote_values = this->get_start_values(); + this->state_.publish_state(); + } + + protected: + LightState &state_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 7c96cda7b1..66329f7cf9 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -13,6 +13,20 @@ AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") +# Color modes +ColorMode = light_ns.enum("ColorMode", is_class=True) +COLOR_MODES = { + "ON_OFF": ColorMode.ON_OFF, + "BRIGHTNESS": ColorMode.BRIGHTNESS, + "WHITE": ColorMode.WHITE, + "COLOR_TEMPERATURE": ColorMode.COLOR_TEMPERATURE, + "COLD_WARM_WHITE": ColorMode.COLD_WARM_WHITE, + "RGB": ColorMode.RGB, + "RGB_WHITE": ColorMode.RGB_WHITE, + "RGB_COLOR_TEMPERATURE": ColorMode.RGB_COLOR_TEMPERATURE, + "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 8d79c96f63..55178941ec 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -7,6 +7,7 @@ from esphome.automation import LambdaAction from esphome.const import ( CONF_ARGS, CONF_BAUD_RATE, + CONF_DEASSERT_RTS_DTR, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, @@ -104,6 +105,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(Logger), cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, + cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection, cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, cv.Optional(CONF_LOGS, default={}): cv.Schema( diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index a8b5d25c61..c7732dfbe3 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_REFERENCE_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -16,16 +15,19 @@ MAX31855Sensor = max31855_ns.class_( ) CONFIG_SCHEMA = ( - sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE) + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ) .extend( { cv.GenerateID(): cv.declare_id(MAX31855Sensor), cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 9583c0bcf9..083d2ac30c 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_MAINS_FILTER, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -23,11 +22,10 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py index 64495ebd7a..33d9c42be3 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -26,7 +25,10 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index ad0e89c028..dff8360226 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor, spi from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -16,7 +15,10 @@ MAX6675Sensor = max6675_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index b130823c12..520694af9d 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -104,7 +104,7 @@ void MAX7219Component::display() { uint8_t pixels[8]; // Run this loop for every MAX CHIP (GRID OF 64 leds) // Run this routine for the rows of every chip 8x row 0 top to 7 bottom - // Fill the pixel parameter with diplay data + // Fill the pixel parameter with display data // Send the data to the chip for (uint8_t i = 0; i < this->num_chips_; i++) { for (uint8_t j = 0; j < 8; j++) { @@ -119,7 +119,7 @@ void MAX7219Component::display() { } int MAX7219Component::get_height_internal() { - return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHE + return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHER // TO BE DONE -> CREATE Virtual size of screen and scroll } @@ -238,7 +238,7 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } else { b = pixels[7 - col]; } - // send this byte to dispay at selected chip + // send this byte to display at selected chip if (this->invert_) { this->send_byte_(col + 1, ~b); } else { diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py index d417f45955..c7f6226e0b 100644 --- a/esphome/components/mcp9808/sensor.py +++ b/esphome/components/mcp9808/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -19,7 +18,10 @@ MCP9808Sensor = mcp9808_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index ebcecb84e2..1a111f7891 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -7,13 +7,11 @@ from esphome.const import ( CONF_CO2, CONF_ID, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, - ICON_EMPTY, ) DEPENDENCIES = ["uart"] @@ -33,18 +31,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MHZ19Component), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, } diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py index 00aa979515..741741fd03 100644 --- a/esphome/components/midea_ac/climate.py +++ b/esphome/components/midea_ac/climate.py @@ -63,21 +63,25 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean, cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean, cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( - UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/monochromatic/monochromatic_light_output.h b/esphome/components/monochromatic/monochromatic_light_output.h index c3a015ff3c..f1708ae70b 100644 --- a/esphome/components/monochromatic/monochromatic_light_output.h +++ b/esphome/components/monochromatic/monochromatic_light_output.h @@ -12,7 +12,7 @@ class MonochromaticLightOutput : public light::LightOutput { void set_output(output::FloatOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index 05c26289b4..f9b61dcadc 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -4,10 +4,8 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, ICON_BRIEFCASE_DOWNLOAD, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER_PER_SECOND_SQUARED, ICON_SCREEN_ROTATION, @@ -30,21 +28,22 @@ MPU6050Component = mpu6050_ns.class_( ) accel_schema = sensor.sensor_schema( - UNIT_METER_PER_SECOND_SQUARED, - ICON_BRIEFCASE_DOWNLOAD, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED, + icon=ICON_BRIEFCASE_DOWNLOAD, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) gyro_schema = sensor.sensor_schema( - UNIT_DEGREE_PER_SECOND, - ICON_SCREEN_ROTATION, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREE_PER_SECOND, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) temperature_schema = sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 3559fce046..56ea9027af 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -92,6 +92,7 @@ MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) +MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) def validate_config(value): diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 25ac430abb..b11ae1fb93 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -61,6 +61,9 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->cover_->get_device_class().empty()) + root["device_class"] = this->cover_->get_device_class(); + auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { root["optimistic"] = true; diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 4bd0882b8c..be662867cf 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -3,6 +3,7 @@ #ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" namespace esphome { namespace mqtt { @@ -14,7 +15,9 @@ std::string MQTTJSONLightComponent::component_type() const { return "light"; } void MQTTJSONLightComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject &root) { - this->state_->make_call().parse_json(root).perform(); + LightCall call = this->state_->make_call(); + LightJSONSchema::parse_json(*this->state_, call, root); + call.perform(); }); auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this); @@ -24,21 +27,40 @@ void MQTTJSONLightComponent::setup() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponent(), state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), [this](JsonObject &root) { this->state_->dump_json(root); }); + return this->publish_json(this->get_state_topic_(), + [this](JsonObject &root) { LightJSONSchema::dump_json(*this->state_, root); }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } std::string MQTTJSONLightComponent::friendly_name() const { return this->state_->get_name(); } void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { root["schema"] = "json"; auto traits = this->state_->get_traits(); - if (traits.get_supports_brightness()) + + root["color_mode"] = true; + JsonArray &color_modes = root.createNestedArray("supported_color_modes"); + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE)) + color_modes.add("color_temp"); + if (traits.supports_color_mode(ColorMode::RGB)) + color_modes.add("rgb"); + if (traits.supports_color_mode(ColorMode::RGB_WHITE)) + color_modes.add("rgbw"); + if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) + color_modes.add("rgbww"); + if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) + color_modes.add("brightness"); + if (traits.supports_color_mode(ColorMode::ON_OFF)) + color_modes.add("onoff"); + + // legacy API + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) root["brightness"] = true; - if (traits.get_supports_rgb()) + if (traits.supports_color_capability(ColorCapability::RGB)) root["rgb"] = true; - if (traits.get_supports_color_temperature()) + if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) root["color_temp"] = true; - if (traits.get_supports_rgb_white_value()) + if (traits.supports_color_capability(ColorCapability::WHITE)) root["white_value"] = true; + if (this->state_->supports_effects()) { root["effect"] = true; JsonArray &effect_list = root.createNestedArray("effect_list"); diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp new file mode 100644 index 0000000000..c0ac472d46 --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -0,0 +1,58 @@ +#include "mqtt_select.h" +#include "esphome/core/log.h" + +#ifdef USE_SELECT + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.select"; + +using namespace esphome::select; + +MQTTSelectComponent::MQTTSelectComponent(Select *select) : MQTTComponent(), select_(select) {} + +void MQTTSelectComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { + auto call = this->select_->make_call(); + call.set_option(state); + call.perform(); + }); + this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); +} + +void MQTTSelectComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, false) +} + +std::string MQTTSelectComponent::component_type() const { return "select"; } + +std::string MQTTSelectComponent::friendly_name() const { return this->select_->get_name(); } +void MQTTSelectComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + const auto &traits = select_->traits; + // https://www.home-assistant.io/integrations/select.mqtt/ + if (!traits.get_icon().empty()) + root["icon"] = traits.get_icon(); + JsonArray &options = root.createNestedArray("options"); + for (const auto &option : traits.get_options()) + options.add(option); + + config.command_topic = true; +} +bool MQTTSelectComponent::send_initial_state() { + if (this->select_->has_state()) { + return this->publish_state(this->select_->state); + } else { + return true; + } +} +bool MQTTSelectComponent::is_internal() { return this->select_->is_internal(); } +bool MQTTSelectComponent::publish_state(const std::string &value) { + return this->publish(this->get_state_topic_(), value); +} + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h new file mode 100644 index 0000000000..013e905ead --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_SELECT + +#include "esphome/components/select/select.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTSelectComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTSelectComponent instance with the provided friendly_name and select + * + * @param select The select. + */ + explicit MQTTSelectComponent(select::Select *select); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + bool is_internal() override; + + bool publish_state(const std::string &value); + + protected: + /// Override for MQTTComponent, returns "select". + std::string component_type() const override; + + std::string friendly_name() const override; + + select::Select *select_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/mqtt_subscribe/sensor/__init__.py b/esphome/components/mqtt_subscribe/sensor/__init__.py index d640b254de..420d4f152c 100644 --- a/esphome/components/mqtt_subscribe/sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/sensor/__init__.py @@ -6,9 +6,6 @@ from esphome.const import ( CONF_QOS, CONF_TOPIC, STATE_CLASS_NONE, - UNIT_EMPTY, - ICON_EMPTY, - DEVICE_CLASS_EMPTY, ) from .. import mqtt_subscribe_ns @@ -21,7 +18,8 @@ MQTTSubscribeSensor = mqtt_subscribe_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py index 34198e04eb..5decb13436 100644 --- a/esphome/components/ms5611/sensor.py +++ b/esphome/components/ms5611/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ICON_GAUGE, @@ -26,18 +25,17 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MS5611Component), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_GAUGE, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 2f279e1c9b..c7f7badc5a 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -115,8 +115,7 @@ class NeoPixelRGBLightOutput : public NeoPixelBusLightOutputBasewriter_ = writer; } -ESPDEPRECATED("set_wait_for_ack(bool) is deprecated and has no effect") -void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "This command is depreciated"); } +ESPDEPRECATED("set_wait_for_ack(bool) is deprecated and has no effect", "v1.20") +void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "This command is deprecated"); } } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2389cc6235..3a43a51975 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -419,7 +419,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * fill_area(50, 50, 100, 100, "RED"); * ``` * - * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to * convert color codes to Nextion HMI colors */ @@ -437,7 +437,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * fill_area(50, 50, 100, 100, color); * ``` * - * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to * convert color codes to Nextion HMI colors */ @@ -546,7 +546,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.filled_cricle(25, 25, 10, "17013"); * ``` * - * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of blue. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to * Nextion HMI colors. */ @@ -563,7 +563,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.filled_cricle(25, 25, 10, color); * ``` * - * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of blue. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to * Nextion HMI colors. */ diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index 6a681af6c5..43c8867e56 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -275,8 +275,8 @@ void Nextion::upload_tft() { } else { #endif ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); - this->transfer_buffer_ = new uint8_t[chunk_size]; - if (!this->transfer_buffer_) { // Try a smaller size + this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; + if (this->transfer_buffer_ == nullptr) { // Try a smaller size ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); chunk_size = 4096; ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); @@ -305,7 +305,7 @@ void Nextion::upload_tft() { App.feed_wdt(); ESP_LOGD(TAG, "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), this->content_length_); } - ESP_LOGD(TAG, "Succesfully updated Nextion!"); + ESP_LOGD(TAG, "Successfully updated Nextion!"); this->upload_end_(); } diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index f8a383e3ac..8a32adc1f6 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -4,10 +4,7 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, - UNIT_EMPTY, - ICON_EMPTY, CONF_COMPONENT_ID, - DEVICE_CLASS_EMPTY, ) from .. import nextion_ns, CONF_NEXTION_ID @@ -46,7 +43,9 @@ def _validate(config): CONFIG_SCHEMA = cv.All( - sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY) + sensor.sensor_schema( + accuracy_decimals=2, + ) .extend( { cv.GenerateID(): cv.declare_id(NextionSensor), diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index e7b8c03586..bf819ffd16 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_VALUE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -120,7 +119,10 @@ def process_calibration(value): CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index bf95cb1b31..88153492d3 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -6,6 +6,7 @@ from esphome.components import mqtt from esphome.const import ( CONF_ABOVE, CONF_BELOW, + CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_ID, CONF_INTERNAL, @@ -45,7 +46,7 @@ NumberInRangeCondition = number_ns.class_( icon = cv.icon -NUMBER_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( +NUMBER_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), cv.GenerateID(): cv.declare_id(Number), @@ -71,6 +72,7 @@ async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: Optional[float] ): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4471794033..4f1fb33fe7 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -17,6 +17,8 @@ from esphome.core import CORE CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +CONF_ZERO_MEANS_ZERO = "zero_means_zero" + BINARY_OUTPUT_SCHEMA = cv.Schema( { cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), @@ -28,6 +30,7 @@ FLOAT_OUTPUT_SCHEMA = BINARY_OUTPUT_SCHEMA.extend( { cv.Optional(CONF_MAX_POWER): cv.percentage, cv.Optional(CONF_MIN_POWER): cv.percentage, + cv.Optional(CONF_ZERO_MEANS_ZERO, default=False): cv.boolean, } ) @@ -53,6 +56,8 @@ async def setup_output_platform_(obj, config): cg.add(obj.set_max_power(config[CONF_MAX_POWER])) if CONF_MIN_POWER in config: cg.add(obj.set_min_power(config[CONF_MIN_POWER])) + if CONF_ZERO_MEANS_ZERO in config: + cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO])) async def register_output(var, config): diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 99ba798cb9..5820a2d7cd 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -17,6 +17,8 @@ void FloatOutput::set_min_power(float min_power) { this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX } +void FloatOutput::set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; } + float FloatOutput::get_min_power() const { return this->min_power_; } void FloatOutput::set_level(float state) { @@ -31,7 +33,7 @@ void FloatOutput::set_level(float state) { #endif if (this->is_inverted()) state = 1.0f - state; - if (state == 0.0f) { // regardless of min_power_, 0.0 means off + if (state == 0.0f && this->zero_means_zero_) { // regardless of min_power_, 0.0 means off this->write_state(state); return; } diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 1b969c9225..3e2b3ada8d 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,6 +46,12 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); + /** Sets this output to ignore min_power for a 0 state + * + * @param zero True if a 0 state should mean 0 and not min_power. + */ + void set_zero_means_zero(bool zero_means_zero); + /** Set the level of this float output, this is called from the front-end. * * @param state The new state. @@ -76,6 +82,7 @@ class FloatOutput : public BinaryOutput { float max_power_{1.0f}; float min_power_{0.0f}; + bool zero_means_zero_; }; } // namespace output diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index 61669d4716..d1007fcbc4 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_GAUGE, @@ -30,7 +29,10 @@ PID_CLIMATE_SENSOR_TYPES = { CONF_CLIMATE_ID = "climate_id" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PERCENT, ICON_GAUGE, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_GAUGE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py new file mode 100644 index 0000000000..20e4672125 --- /dev/null +++ b/esphome/components/pipsolar/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.components import uart + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@andreashergert1984"] +AUTO_LOAD = ["binary_sensor", "text_sensor", "sensor", "switch", "output"] +MULTI_CONF = True + +CONF_PIPSOLAR_ID = "pipsolar_id" + +pipsolar_ns = cg.esphome_ns.namespace("pipsolar") +PipsolarComponent = pipsolar_ns.class_("Pipsolar", cg.Component) + +PIPSOLAR_COMPONENT_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.Required(CONF_PIPSOLAR_ID): cv.use_id(PipsolarComponent), + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema({cv.GenerateID(): cv.declare_id(PipsolarComponent)}) + .extend(cv.polling_component_schema("1s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) diff --git a/esphome/components/pipsolar/binary_sensor/__init__.py b/esphome/components/pipsolar/binary_sensor/__init__.py new file mode 100644 index 0000000000..5c6af3bffc --- /dev/null +++ b/esphome/components/pipsolar/binary_sensor/__init__.py @@ -0,0 +1,144 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, +) +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID + +DEPENDENCIES = ["uart"] + +CONF_ADD_SBU_PRIORITY_VERSION = "add_sbu_priority_version" +CONF_CONFIGURATION_STATUS = "configuration_status" +CONF_SCC_FIRMWARE_VERSION = "scc_firmware_version" +CONF_LOAD_STATUS = "load_status" +CONF_BATTERY_VOLTAGE_TO_STEADY_WHILE_CHARGING = ( + "battery_voltage_to_steady_while_charging" +) +CONF_CHARGING_STATUS = "charging_status" +CONF_SCC_CHARGING_STATUS = "scc_charging_status" +CONF_AC_CHARGING_STATUS = "ac_charging_status" +CONF_CHARGING_TO_FLOATING_MODE = "charging_to_floating_mode" +CONF_SWITCH_ON = "switch_on" +CONF_DUSTPROOF_INSTALLED = "dustproof_installed" +CONF_SILENCE_BUZZER_OPEN_BUZZER = "silence_buzzer_open_buzzer" +CONF_OVERLOAD_BYPASS_FUNCTION = "overload_bypass_function" +CONF_LCD_ESCAPE_TO_DEFAULT = "lcd_escape_to_default" +CONF_OVERLOAD_RESTART_FUNCTION = "overload_restart_function" +CONF_OVER_TEMPERATURE_RESTART_FUNCTION = "over_temperature_restart_function" +CONF_BACKLIGHT_ON = "backlight_on" +CONF_ALARM_ON_WHEN_PRIMARY_SOURCE_INTERRUPT = "alarm_on_when_primary_source_interrupt" +CONF_FAULT_CODE_RECORD = "fault_code_record" +CONF_POWER_SAVING = "power_saving" + +CONF_WARNINGS_PRESENT = "warnings_present" +CONF_FAULTS_PRESENT = "faults_present" +CONF_WARNING_POWER_LOSS = "warning_power_loss" +CONF_FAULT_INVERTER_FAULT = "fault_inverter_fault" +CONF_FAULT_BUS_OVER = "fault_bus_over" +CONF_FAULT_BUS_UNDER = "fault_bus_under" +CONF_FAULT_BUS_SOFT_FAIL = "fault_bus_soft_fail" +CONF_WARNING_LINE_FAIL = "warning_line_fail" +CONF_FAULT_OPVSHORT = "fault_opvshort" +CONF_FAULT_INVERTER_VOLTAGE_TOO_LOW = "fault_inverter_voltage_too_low" +CONF_FAULT_INVERTER_VOLTAGE_TOO_HIGH = "fault_inverter_voltage_too_high" +CONF_WARNING_OVER_TEMPERATURE = "warning_over_temperature" +CONF_WARNING_FAN_LOCK = "warning_fan_lock" +CONF_WARNING_BATTERY_VOLTAGE_HIGH = "warning_battery_voltage_high" +CONF_WARNING_BATTERY_LOW_ALARM = "warning_battery_low_alarm" +CONF_WARNING_BATTERY_UNDER_SHUTDOWN = "warning_battery_under_shutdown" +CONF_WARNING_BATTERY_DERATING = "warning_battery_derating" +CONF_WARNING_OVER_LOAD = "warning_over_load" +CONF_WARNING_EEPROM_FAILED = "warning_eeprom_failed" +CONF_FAULT_INVERTER_OVER_CURRENT = "fault_inverter_over_current" +CONF_FAULT_INVERTER_SOFT_FAILED = "fault_inverter_soft_failed" +CONF_FAULT_SELF_TEST_FAILED = "fault_self_test_failed" +CONF_FAULT_OP_DC_VOLTAGE_OVER = "fault_op_dc_voltage_over" +CONF_FAULT_BATTERY_OPEN = "fault_battery_open" +CONF_FAULT_CURRENT_SENSOR_FAILED = "fault_current_sensor_failed" +CONF_FAULT_BATTERY_SHORT = "fault_battery_short" +CONF_WARNING_POWER_LIMIT = "warning_power_limit" +CONF_WARNING_PV_VOLTAGE_HIGH = "warning_pv_voltage_high" +CONF_FAULT_MPPT_OVERLOAD = "fault_mppt_overload" +CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload" +CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge" +CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current" +CONF_FAULT_CODE = "fault_code" +CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy" +CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = ( + "warning_high_ac_input_during_bus_soft_start" +) +CONF_WARNING_BATTERY_EQUALIZATION = "warning_battery_equalization" + +TYPES = [ + CONF_ADD_SBU_PRIORITY_VERSION, + CONF_CONFIGURATION_STATUS, + CONF_SCC_FIRMWARE_VERSION, + CONF_LOAD_STATUS, + CONF_BATTERY_VOLTAGE_TO_STEADY_WHILE_CHARGING, + CONF_CHARGING_STATUS, + CONF_SCC_CHARGING_STATUS, + CONF_AC_CHARGING_STATUS, + CONF_CHARGING_TO_FLOATING_MODE, + CONF_SWITCH_ON, + CONF_DUSTPROOF_INSTALLED, + CONF_SILENCE_BUZZER_OPEN_BUZZER, + CONF_OVERLOAD_BYPASS_FUNCTION, + CONF_LCD_ESCAPE_TO_DEFAULT, + CONF_OVERLOAD_RESTART_FUNCTION, + CONF_OVER_TEMPERATURE_RESTART_FUNCTION, + CONF_BACKLIGHT_ON, + CONF_ALARM_ON_WHEN_PRIMARY_SOURCE_INTERRUPT, + CONF_FAULT_CODE_RECORD, + CONF_POWER_SAVING, + CONF_WARNINGS_PRESENT, + CONF_FAULTS_PRESENT, + CONF_WARNING_POWER_LOSS, + CONF_FAULT_INVERTER_FAULT, + CONF_FAULT_BUS_OVER, + CONF_FAULT_BUS_UNDER, + CONF_FAULT_BUS_SOFT_FAIL, + CONF_WARNING_LINE_FAIL, + CONF_FAULT_OPVSHORT, + CONF_FAULT_INVERTER_VOLTAGE_TOO_LOW, + CONF_FAULT_INVERTER_VOLTAGE_TOO_HIGH, + CONF_WARNING_OVER_TEMPERATURE, + CONF_WARNING_FAN_LOCK, + CONF_WARNING_BATTERY_VOLTAGE_HIGH, + CONF_WARNING_BATTERY_LOW_ALARM, + CONF_WARNING_BATTERY_UNDER_SHUTDOWN, + CONF_WARNING_BATTERY_DERATING, + CONF_WARNING_OVER_LOAD, + CONF_WARNING_EEPROM_FAILED, + CONF_FAULT_INVERTER_OVER_CURRENT, + CONF_FAULT_INVERTER_SOFT_FAILED, + CONF_FAULT_SELF_TEST_FAILED, + CONF_FAULT_OP_DC_VOLTAGE_OVER, + CONF_FAULT_BATTERY_OPEN, + CONF_FAULT_CURRENT_SENSOR_FAILED, + CONF_FAULT_BATTERY_SHORT, + CONF_WARNING_POWER_LIMIT, + CONF_WARNING_PV_VOLTAGE_HIGH, + CONF_FAULT_MPPT_OVERLOAD, + CONF_WARNING_MPPT_OVERLOAD, + CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE, + CONF_FAULT_DC_DC_OVER_CURRENT, + CONF_FAULT_CODE, + CONF_WARNUNG_LOW_PV_ENERGY, + CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, + CONF_WARNING_BATTERY_EQUALIZATION, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): binary_sensor.BINARY_SENSOR_SCHEMA for type in TYPES} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + for type in TYPES: + if type in config: + conf = config[type] + sens = cg.new_Pvariable(conf[CONF_ID]) + await binary_sensor.register_binary_sensor(sens, conf) + cg.add(getattr(paren, f"set_{type}")(sens)) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py new file mode 100644 index 0000000000..b518d485e7 --- /dev/null +++ b/esphome/components/pipsolar/output/__init__.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import output +from esphome.const import CONF_ID, CONF_VALUE +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID, pipsolar_ns + +DEPENDENCIES = ["pipsolar"] + +PipsolarOutput = pipsolar_ns.class_("PipsolarOutput", output.FloatOutput) +SetOutputAction = pipsolar_ns.class_("SetOutputAction", automation.Action) + +CONF_POSSIBLE_VALUES = "possible_values" + +# 3.11 PCVV: Setting battery C.V. (constant voltage) charging voltage 48.0V ~ 58.4V for 48V unit +# battery_bulk_voltage; +# battery_recharge_voltage; 12V unit: 11V/11.3V/11.5V/11.8V/12V/12.3V/12.5V/12.8V +# 24V unit: 22V/22.5V/23V/23.5V/24V/24.5V/25V/25.5V +# 48V unit: 44V/45V/46V/47V/48V/49V/50V/51V +# battery_under_voltage; 40.0V ~ 48.0V for 48V unit +# battery_float_voltage; 48.0V ~ 58.4V for 48V unit +# battery_type; 00 for AGM, 01 for Flooded battery +# current_max_ac_charging_current; +# output_source_priority; 00 / 01 / 02 +# charger_source_priority; For HS: 00 for utility first, 01 for solar first, 02 for solar and utility, 03 for only solar charging +# For MS/MSX: 00 for utility first, 01 for solar first, 03 for only solar charging +# battery_redischarge_voltage; 12V unit: 00.0V12V/12.3V/12.5V/12.8V/13V/13.3V/13.5V/13.8V/14V/14.3V/14.5 +# 24V unit: 00.0V/24V/24.5V/25V/25.5V/26V/26.5V/27V/27.5V/28V/28.5V/29V +# 48V unit: 00.0V48V/49V/50V/51V/52V/53V/54V/55V/56V/57V/58V + +CONF_BATTERY_RECHARGE_VOLTAGE = "battery_recharge_voltage" +CONF_BATTERY_UNDER_VOLTAGE = "battery_under_voltage" +CONF_BATTERY_FLOAT_VOLTAGE = "battery_float_voltage" +CONF_BATTERY_TYPE = "battery_type" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_BATTERY_REDISCHARGE_VOLTAGE = "battery_redischarge_voltage" + +TYPES = { + CONF_BATTERY_RECHARGE_VOLTAGE: ( + [44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0], + "PBCV%02.1f", + ), + CONF_BATTERY_UNDER_VOLTAGE: ( + [40.0, 40.1, 42, 43, 44, 45, 46, 47, 48.0], + "PSDV%02.1f", + ), + CONF_BATTERY_FLOAT_VOLTAGE: ([48.0, 49.0, 50.0, 51.0], "PBFT%02.1f"), + CONF_BATTERY_TYPE: ([0, 1, 2], "PBT%02.0f"), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: ([2, 10, 20], "MUCHGC0%02.0f"), + CONF_CURRENT_MAX_CHARGING_CURRENT: ([10, 20, 30, 40], "MCHGC0%02.0f"), + CONF_OUTPUT_SOURCE_PRIORITY: ([0, 1, 2], "POP%02.0f"), + CONF_CHARGER_SOURCE_PRIORITY: ([0, 1, 2, 3], "PCP%02.0f"), + CONF_BATTERY_REDISCHARGE_VOLTAGE: ( + [0, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58], + "PBDV%02.1f", + ), +} + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + { + cv.Optional(type): output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(PipsolarOutput), + cv.Optional(CONF_POSSIBLE_VALUES, default=values): cv.All( + cv.ensure_list(cv.positive_float), cv.Length(min=1) + ), + } + ) + for type, (values, _) in TYPES.items() + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, (_, command) in TYPES.items(): + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await output.register_output(var, conf) + cg.add(var.set_parent(paren)) + cg.add(var.set_set_command(command)) + if (CONF_POSSIBLE_VALUES) in conf: + cg.add(var.set_possible_values(conf[CONF_POSSIBLE_VALUES])) + + +@automation.register_action( + "output.pipsolar.set_level", + SetOutputAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(CONF_ID), + cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), + } + ), +) +def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_VALUE], args, float) + cg.add(var.set_level(template_)) + yield var diff --git a/esphome/components/pipsolar/output/pipsolar_output.cpp b/esphome/components/pipsolar/output/pipsolar_output.cpp new file mode 100644 index 0000000000..b843f1f3e6 --- /dev/null +++ b/esphome/components/pipsolar/output/pipsolar_output.cpp @@ -0,0 +1,22 @@ +#include "pipsolar_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.output"; + +void PipsolarOutput::write_state(float state) { + char tmp[10]; + sprintf(tmp, this->set_command_.c_str(), state); + + if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) { + ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state); + this->parent_->switch_command(std::string(tmp)); + } else { + ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp); + } +} +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h new file mode 100644 index 0000000000..932efe01c2 --- /dev/null +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -0,0 +1,40 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { + +class Pipsolar; + +class PipsolarOutput : public output::FloatOutput { + public: + PipsolarOutput() {} + void set_parent(Pipsolar *parent) { this->parent_ = parent; } + void set_set_command(std::string command) { this->set_command_ = std::move(command); }; + void set_possible_values(std::vector possible_values) { this->possible_values_ = std::move(possible_values); } + void set_value(float value) { this->write_state(value); }; + + protected: + void write_state(float state) override; + std::string set_command_; + Pipsolar *parent_; + std::vector possible_values_; +}; + +template class SetOutputAction : public Action { + public: + SetOutputAction(PipsolarOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, level) + + void play(Ts... x) override { this->output_->set_value(this->level_.value(x...)); } + + protected: + PipsolarOutput *output_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp new file mode 100644 index 0000000000..9c7adc13c1 --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -0,0 +1,922 @@ +#include "pipsolar.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar"; + +void Pipsolar::setup() { + this->state_ = STATE_IDLE; + this->command_start_millis_ = 0; +} + +void Pipsolar::empty_uart_buffer_() { + uint8_t byte; + while (this->available()) { + this->read_byte(&byte); + } +} + +void Pipsolar::loop() { + // Read message + if (this->state_ == STATE_IDLE) { + this->empty_uart_buffer_(); + switch (this->send_next_command_()) { + case 0: + // no command send (empty queue) time to poll + if (millis() - this->last_poll_ > this->update_interval_) { + this->send_next_poll_(); + this->last_poll_ = millis(); + } + return; + break; + case 1: + // command send + return; + break; + } + } + if (this->state_ == STATE_COMMAND_COMPLETE) { + if (this->check_incoming_length_(4)) { + ESP_LOGD(TAG, "response length for command OK"); + if (this->check_incoming_crc_()) { + // crc ok + if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { + ESP_LOGD(TAG, "command successful"); + } else { + ESP_LOGD(TAG, "command not successful"); + } + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + + } else { + // crc failed + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + } + } else { + ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", + this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + } + } + + if (this->state_ == STATE_POLL_DECODED) { + std::string mode; + switch (this->used_polling_commands_[this->last_polling_command_].identifier) { + case POLLING_QPIRI: + if (this->grid_rating_voltage_) { + this->grid_rating_voltage_->publish_state(value_grid_rating_voltage_); + } + if (this->grid_rating_current_) { + this->grid_rating_current_->publish_state(value_grid_rating_current_); + } + if (this->ac_output_rating_voltage_) { + this->ac_output_rating_voltage_->publish_state(value_ac_output_rating_voltage_); + } + if (this->ac_output_rating_frequency_) { + this->ac_output_rating_frequency_->publish_state(value_ac_output_rating_frequency_); + } + if (this->ac_output_rating_current_) { + this->ac_output_rating_current_->publish_state(value_ac_output_rating_current_); + } + if (this->ac_output_rating_apparent_power_) { + this->ac_output_rating_apparent_power_->publish_state(value_ac_output_rating_apparent_power_); + } + if (this->ac_output_rating_active_power_) { + this->ac_output_rating_active_power_->publish_state(value_ac_output_rating_active_power_); + } + if (this->battery_rating_voltage_) { + this->battery_rating_voltage_->publish_state(value_battery_rating_voltage_); + } + if (this->battery_recharge_voltage_) { + this->battery_recharge_voltage_->publish_state(value_battery_recharge_voltage_); + } + if (this->battery_under_voltage_) { + this->battery_under_voltage_->publish_state(value_battery_under_voltage_); + } + if (this->battery_bulk_voltage_) { + this->battery_bulk_voltage_->publish_state(value_battery_bulk_voltage_); + } + if (this->battery_float_voltage_) { + this->battery_float_voltage_->publish_state(value_battery_float_voltage_); + } + if (this->battery_type_) { + this->battery_type_->publish_state(value_battery_type_); + } + if (this->current_max_ac_charging_current_) { + this->current_max_ac_charging_current_->publish_state(value_current_max_ac_charging_current_); + } + if (this->current_max_charging_current_) { + this->current_max_charging_current_->publish_state(value_current_max_charging_current_); + } + if (this->input_voltage_range_) { + this->input_voltage_range_->publish_state(value_input_voltage_range_); + } + // special for input voltage range switch + if (this->input_voltage_range_switch_) { + this->input_voltage_range_switch_->publish_state(value_input_voltage_range_ == 1); + } + if (this->output_source_priority_) { + this->output_source_priority_->publish_state(value_output_source_priority_); + } + // special for output source priority switches + if (this->output_source_priority_utility_switch_) { + this->output_source_priority_utility_switch_->publish_state(value_output_source_priority_ == 0); + } + if (this->output_source_priority_solar_switch_) { + this->output_source_priority_solar_switch_->publish_state(value_output_source_priority_ == 1); + } + if (this->output_source_priority_battery_switch_) { + this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); + } + if (this->charger_source_priority_) { + this->charger_source_priority_->publish_state(value_charger_source_priority_); + } + if (this->parallel_max_num_) { + this->parallel_max_num_->publish_state(value_parallel_max_num_); + } + if (this->machine_type_) { + this->machine_type_->publish_state(value_machine_type_); + } + if (this->topology_) { + this->topology_->publish_state(value_topology_); + } + if (this->output_mode_) { + this->output_mode_->publish_state(value_output_mode_); + } + if (this->battery_redischarge_voltage_) { + this->battery_redischarge_voltage_->publish_state(value_battery_redischarge_voltage_); + } + if (this->pv_ok_condition_for_parallel_) { + this->pv_ok_condition_for_parallel_->publish_state(value_pv_ok_condition_for_parallel_); + } + // special for pv ok condition switch + if (this->pv_ok_condition_for_parallel_switch_) { + this->pv_ok_condition_for_parallel_switch_->publish_state(value_pv_ok_condition_for_parallel_ == 1); + } + if (this->pv_power_balance_) { + this->pv_power_balance_->publish_state(value_pv_power_balance_ == 1); + } + // special for power balance switch + if (this->pv_power_balance_switch_) { + this->pv_power_balance_switch_->publish_state(value_pv_power_balance_ == 1); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QPIGS: + if (this->grid_voltage_) { + this->grid_voltage_->publish_state(value_grid_voltage_); + } + if (this->grid_frequency_) { + this->grid_frequency_->publish_state(value_grid_frequency_); + } + if (this->ac_output_voltage_) { + this->ac_output_voltage_->publish_state(value_ac_output_voltage_); + } + if (this->ac_output_frequency_) { + this->ac_output_frequency_->publish_state(value_ac_output_frequency_); + } + if (this->ac_output_apparent_power_) { + this->ac_output_apparent_power_->publish_state(value_ac_output_apparent_power_); + } + if (this->ac_output_active_power_) { + this->ac_output_active_power_->publish_state(value_ac_output_active_power_); + } + if (this->output_load_percent_) { + this->output_load_percent_->publish_state(value_output_load_percent_); + } + if (this->bus_voltage_) { + this->bus_voltage_->publish_state(value_bus_voltage_); + } + if (this->battery_voltage_) { + this->battery_voltage_->publish_state(value_battery_voltage_); + } + if (this->battery_charging_current_) { + this->battery_charging_current_->publish_state(value_battery_charging_current_); + } + if (this->battery_capacity_percent_) { + this->battery_capacity_percent_->publish_state(value_battery_capacity_percent_); + } + if (this->inverter_heat_sink_temperature_) { + this->inverter_heat_sink_temperature_->publish_state(value_inverter_heat_sink_temperature_); + } + if (this->pv_input_current_for_battery_) { + this->pv_input_current_for_battery_->publish_state(value_pv_input_current_for_battery_); + } + if (this->pv_input_voltage_) { + this->pv_input_voltage_->publish_state(value_pv_input_voltage_); + } + if (this->battery_voltage_scc_) { + this->battery_voltage_scc_->publish_state(value_battery_voltage_scc_); + } + if (this->battery_discharge_current_) { + this->battery_discharge_current_->publish_state(value_battery_discharge_current_); + } + if (this->add_sbu_priority_version_) { + this->add_sbu_priority_version_->publish_state(value_add_sbu_priority_version_); + } + if (this->configuration_status_) { + this->configuration_status_->publish_state(value_configuration_status_); + } + if (this->scc_firmware_version_) { + this->scc_firmware_version_->publish_state(value_scc_firmware_version_); + } + if (this->load_status_) { + this->load_status_->publish_state(value_load_status_); + } + if (this->battery_voltage_to_steady_while_charging_) { + this->battery_voltage_to_steady_while_charging_->publish_state( + value_battery_voltage_to_steady_while_charging_); + } + if (this->charging_status_) { + this->charging_status_->publish_state(value_charging_status_); + } + if (this->scc_charging_status_) { + this->scc_charging_status_->publish_state(value_scc_charging_status_); + } + if (this->ac_charging_status_) { + this->ac_charging_status_->publish_state(value_ac_charging_status_); + } + if (this->battery_voltage_offset_for_fans_on_) { + this->battery_voltage_offset_for_fans_on_->publish_state(value_battery_voltage_offset_for_fans_on_ / 10.0f); + } //.1 scale + if (this->eeprom_version_) { + this->eeprom_version_->publish_state(value_eeprom_version_); + } + if (this->pv_charging_power_) { + this->pv_charging_power_->publish_state(value_pv_charging_power_); + } + if (this->charging_to_floating_mode_) { + this->charging_to_floating_mode_->publish_state(value_charging_to_floating_mode_); + } + if (this->switch_on_) { + this->switch_on_->publish_state(value_switch_on_); + } + if (this->dustproof_installed_) { + this->dustproof_installed_->publish_state(value_dustproof_installed_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QMOD: + if (this->device_mode_) { + mode = value_device_mode_; + this->device_mode_->publish_state(mode); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QFLAG: + if (this->silence_buzzer_open_buzzer_) { + this->silence_buzzer_open_buzzer_->publish_state(value_silence_buzzer_open_buzzer_); + } + if (this->overload_bypass_function_) { + this->overload_bypass_function_->publish_state(value_overload_bypass_function_); + } + if (this->lcd_escape_to_default_) { + this->lcd_escape_to_default_->publish_state(value_lcd_escape_to_default_); + } + if (this->overload_restart_function_) { + this->overload_restart_function_->publish_state(value_overload_restart_function_); + } + if (this->over_temperature_restart_function_) { + this->over_temperature_restart_function_->publish_state(value_over_temperature_restart_function_); + } + if (this->backlight_on_) { + this->backlight_on_->publish_state(value_backlight_on_); + } + if (this->alarm_on_when_primary_source_interrupt_) { + this->alarm_on_when_primary_source_interrupt_->publish_state(value_alarm_on_when_primary_source_interrupt_); + } + if (this->fault_code_record_) { + this->fault_code_record_->publish_state(value_fault_code_record_); + } + if (this->power_saving_) { + this->power_saving_->publish_state(value_power_saving_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QPIWS: + if (this->warnings_present_) { + this->warnings_present_->publish_state(value_warnings_present_); + } + if (this->faults_present_) { + this->faults_present_->publish_state(value_faults_present_); + } + if (this->warning_power_loss_) { + this->warning_power_loss_->publish_state(value_warning_power_loss_); + } + if (this->fault_inverter_fault_) { + this->fault_inverter_fault_->publish_state(value_fault_inverter_fault_); + } + if (this->fault_bus_over_) { + this->fault_bus_over_->publish_state(value_fault_bus_over_); + } + if (this->fault_bus_under_) { + this->fault_bus_under_->publish_state(value_fault_bus_under_); + } + if (this->fault_bus_soft_fail_) { + this->fault_bus_soft_fail_->publish_state(value_fault_bus_soft_fail_); + } + if (this->warning_line_fail_) { + this->warning_line_fail_->publish_state(value_warning_line_fail_); + } + if (this->fault_opvshort_) { + this->fault_opvshort_->publish_state(value_fault_opvshort_); + } + if (this->fault_inverter_voltage_too_low_) { + this->fault_inverter_voltage_too_low_->publish_state(value_fault_inverter_voltage_too_low_); + } + if (this->fault_inverter_voltage_too_high_) { + this->fault_inverter_voltage_too_high_->publish_state(value_fault_inverter_voltage_too_high_); + } + if (this->warning_over_temperature_) { + this->warning_over_temperature_->publish_state(value_warning_over_temperature_); + } + if (this->warning_fan_lock_) { + this->warning_fan_lock_->publish_state(value_warning_fan_lock_); + } + if (this->warning_battery_voltage_high_) { + this->warning_battery_voltage_high_->publish_state(value_warning_battery_voltage_high_); + } + if (this->warning_battery_low_alarm_) { + this->warning_battery_low_alarm_->publish_state(value_warning_battery_low_alarm_); + } + if (this->warning_battery_under_shutdown_) { + this->warning_battery_under_shutdown_->publish_state(value_warning_battery_under_shutdown_); + } + if (this->warning_battery_derating_) { + this->warning_battery_derating_->publish_state(value_warning_battery_derating_); + } + if (this->warning_over_load_) { + this->warning_over_load_->publish_state(value_warning_over_load_); + } + if (this->warning_eeprom_failed_) { + this->warning_eeprom_failed_->publish_state(value_warning_eeprom_failed_); + } + if (this->fault_inverter_over_current_) { + this->fault_inverter_over_current_->publish_state(value_fault_inverter_over_current_); + } + if (this->fault_inverter_soft_failed_) { + this->fault_inverter_soft_failed_->publish_state(value_fault_inverter_soft_failed_); + } + if (this->fault_self_test_failed_) { + this->fault_self_test_failed_->publish_state(value_fault_self_test_failed_); + } + if (this->fault_op_dc_voltage_over_) { + this->fault_op_dc_voltage_over_->publish_state(value_fault_op_dc_voltage_over_); + } + if (this->fault_battery_open_) { + this->fault_battery_open_->publish_state(value_fault_battery_open_); + } + if (this->fault_current_sensor_failed_) { + this->fault_current_sensor_failed_->publish_state(value_fault_current_sensor_failed_); + } + if (this->fault_battery_short_) { + this->fault_battery_short_->publish_state(value_fault_battery_short_); + } + if (this->warning_power_limit_) { + this->warning_power_limit_->publish_state(value_warning_power_limit_); + } + if (this->warning_pv_voltage_high_) { + this->warning_pv_voltage_high_->publish_state(value_warning_pv_voltage_high_); + } + if (this->fault_mppt_overload_) { + this->fault_mppt_overload_->publish_state(value_fault_mppt_overload_); + } + if (this->warning_mppt_overload_) { + this->warning_mppt_overload_->publish_state(value_warning_mppt_overload_); + } + if (this->warning_battery_too_low_to_charge_) { + this->warning_battery_too_low_to_charge_->publish_state(value_warning_battery_too_low_to_charge_); + } + if (this->fault_dc_dc_over_current_) { + this->fault_dc_dc_over_current_->publish_state(value_fault_dc_dc_over_current_); + } + if (this->fault_code_) { + this->fault_code_->publish_state(value_fault_code_); + } + if (this->warnung_low_pv_energy_) { + this->warnung_low_pv_energy_->publish_state(value_warnung_low_pv_energy_); + } + if (this->warning_high_ac_input_during_bus_soft_start_) { + this->warning_high_ac_input_during_bus_soft_start_->publish_state( + value_warning_high_ac_input_during_bus_soft_start_); + } + if (this->warning_battery_equalization_) { + this->warning_battery_equalization_->publish_state(value_warning_battery_equalization_); + } + this->state_ = STATE_IDLE; + break; + case POLLING_QT: + this->state_ = STATE_IDLE; + break; + case POLLING_QMN: + this->state_ = STATE_IDLE; + break; + } + } + + if (this->state_ == STATE_POLL_CHECKED) { + bool enabled = true; + std::string fc; + char tmp[PIPSOLAR_READ_BUFFER_LENGTH]; + sprintf(tmp, "%s", this->read_buffer_); + switch (this->used_polling_commands_[this->last_polling_command_].identifier) { + case POLLING_QPIRI: + ESP_LOGD(TAG, "Decode QPIRI"); + sscanf(tmp, "(%f %f %f %f %f %d %d %f %f %f %f %f %d %d %d %d %d %d %d %d %d %d %f %d %d", // NOLINT + &value_grid_rating_voltage_, &value_grid_rating_current_, &value_ac_output_rating_voltage_, // NOLINT + &value_ac_output_rating_frequency_, &value_ac_output_rating_current_, // NOLINT + &value_ac_output_rating_apparent_power_, &value_ac_output_rating_active_power_, // NOLINT + &value_battery_rating_voltage_, &value_battery_recharge_voltage_, // NOLINT + &value_battery_under_voltage_, &value_battery_bulk_voltage_, &value_battery_float_voltage_, // NOLINT + &value_battery_type_, &value_current_max_ac_charging_current_, // NOLINT + &value_current_max_charging_current_, &value_input_voltage_range_, // NOLINT + &value_output_source_priority_, &value_charger_source_priority_, &value_parallel_max_num_, // NOLINT + &value_machine_type_, &value_topology_, &value_output_mode_, // NOLINT + &value_battery_redischarge_voltage_, &value_pv_ok_condition_for_parallel_, // NOLINT + &value_pv_power_balance_); // NOLINT + if (this->last_qpiri_) { + this->last_qpiri_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QPIGS: + ESP_LOGD(TAG, "Decode QPIGS"); + sscanf( // NOLINT + tmp, // NOLINT + "(%f %f %f %f %d %d %d %d %f %d %d %d %d %f %f %d %1d%1d%1d%1d%1d%1d%1d%1d %d %d %d %1d%1d%1d", // NOLINT + &value_grid_voltage_, &value_grid_frequency_, &value_ac_output_voltage_, // NOLINT + &value_ac_output_frequency_, // NOLINT + &value_ac_output_apparent_power_, &value_ac_output_active_power_, &value_output_load_percent_, // NOLINT + &value_bus_voltage_, &value_battery_voltage_, &value_battery_charging_current_, // NOLINT + &value_battery_capacity_percent_, &value_inverter_heat_sink_temperature_, // NOLINT + &value_pv_input_current_for_battery_, &value_pv_input_voltage_, &value_battery_voltage_scc_, // NOLINT + &value_battery_discharge_current_, &value_add_sbu_priority_version_, // NOLINT + &value_configuration_status_, &value_scc_firmware_version_, &value_load_status_, // NOLINT + &value_battery_voltage_to_steady_while_charging_, &value_charging_status_, // NOLINT + &value_scc_charging_status_, &value_ac_charging_status_, // NOLINT + &value_battery_voltage_offset_for_fans_on_, &value_eeprom_version_, &value_pv_charging_power_, // NOLINT + &value_charging_to_floating_mode_, &value_switch_on_, // NOLINT + &value_dustproof_installed_); // NOLINT + if (this->last_qpigs_) { + this->last_qpigs_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QMOD: + ESP_LOGD(TAG, "Decode QMOD"); + this->value_device_mode_ = char(this->read_buffer_[1]); + if (this->last_qmod_) { + this->last_qmod_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QFLAG: + ESP_LOGD(TAG, "Decode QFLAG"); + // result like:"(EbkuvxzDajy" + // get through all char: ignore first "(" Enable flag on 'E', Disable on 'D') else set the corresponding value + for (int i = 1; i < strlen(tmp); i++) { + switch (tmp[i]) { + case 'E': + enabled = true; + break; + case 'D': + enabled = false; + break; + case 'a': + this->value_silence_buzzer_open_buzzer_ = enabled; + break; + case 'b': + this->value_overload_bypass_function_ = enabled; + break; + case 'k': + this->value_lcd_escape_to_default_ = enabled; + break; + case 'u': + this->value_overload_restart_function_ = enabled; + break; + case 'v': + this->value_over_temperature_restart_function_ = enabled; + break; + case 'x': + this->value_backlight_on_ = enabled; + break; + case 'y': + this->value_alarm_on_when_primary_source_interrupt_ = enabled; + break; + case 'z': + this->value_fault_code_record_ = enabled; + break; + case 'j': + this->value_power_saving_ = enabled; + break; + } + } + if (this->last_qflag_) { + this->last_qflag_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QPIWS: + ESP_LOGD(TAG, "Decode QPIWS"); + // '(00000000000000000000000000000000' + // iterate over all available flag (as not all models have all flags, but at least in the same order) + this->value_warnings_present_ = false; + this->value_faults_present_ = true; + + for (int i = 1; i < strlen(tmp); i++) { + enabled = tmp[i] == '1'; + switch (i) { + case 1: + this->value_warning_power_loss_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 2: + this->value_fault_inverter_fault_ = enabled; + this->value_faults_present_ += enabled; + break; + case 3: + this->value_fault_bus_over_ = enabled; + this->value_faults_present_ += enabled; + break; + case 4: + this->value_fault_bus_under_ = enabled; + this->value_faults_present_ += enabled; + break; + case 5: + this->value_fault_bus_soft_fail_ = enabled; + this->value_faults_present_ += enabled; + break; + case 6: + this->value_warning_line_fail_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 7: + this->value_fault_opvshort_ = enabled; + this->value_faults_present_ += enabled; + break; + case 8: + this->value_fault_inverter_voltage_too_low_ = enabled; + this->value_faults_present_ += enabled; + break; + case 9: + this->value_fault_inverter_voltage_too_high_ = enabled; + this->value_faults_present_ += enabled; + break; + case 10: + this->value_warning_over_temperature_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 11: + this->value_warning_fan_lock_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 12: + this->value_warning_battery_voltage_high_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 13: + this->value_warning_battery_low_alarm_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 15: + this->value_warning_battery_under_shutdown_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 16: + this->value_warning_battery_derating_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 17: + this->value_warning_over_load_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 18: + this->value_warning_eeprom_failed_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 19: + this->value_fault_inverter_over_current_ = enabled; + this->value_faults_present_ += enabled; + break; + case 20: + this->value_fault_inverter_soft_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 21: + this->value_fault_self_test_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 22: + this->value_fault_op_dc_voltage_over_ = enabled; + this->value_faults_present_ += enabled; + break; + case 23: + this->value_fault_battery_open_ = enabled; + this->value_faults_present_ += enabled; + break; + case 24: + this->value_fault_current_sensor_failed_ = enabled; + this->value_faults_present_ += enabled; + break; + case 25: + this->value_fault_battery_short_ = enabled; + this->value_faults_present_ += enabled; + break; + case 26: + this->value_warning_power_limit_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 27: + this->value_warning_pv_voltage_high_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 28: + this->value_fault_mppt_overload_ = enabled; + this->value_faults_present_ += enabled; + break; + case 29: + this->value_warning_mppt_overload_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 30: + this->value_warning_battery_too_low_to_charge_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 31: + this->value_fault_dc_dc_over_current_ = enabled; + this->value_faults_present_ += enabled; + break; + case 32: + fc = tmp[i]; + fc += tmp[i + 1]; + this->value_fault_code_ = strtol(fc.c_str(), nullptr, 10); + break; + case 34: + this->value_warnung_low_pv_energy_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 35: + this->value_warning_high_ac_input_during_bus_soft_start_ = enabled; + this->value_warnings_present_ += enabled; + break; + case 36: + this->value_warning_battery_equalization_ = enabled; + this->value_warnings_present_ += enabled; + break; + } + } + if (this->last_qpiws_) { + this->last_qpiws_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QT: + ESP_LOGD(TAG, "Decode QT"); + if (this->last_qt_) { + this->last_qt_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + case POLLING_QMN: + ESP_LOGD(TAG, "Decode QMN"); + if (this->last_qmn_) { + this->last_qmn_->publish_state(tmp); + } + this->state_ = STATE_POLL_DECODED; + break; + default: + this->state_ = STATE_IDLE; + break; + } + return; + } + + if (this->state_ == STATE_POLL_COMPLETE) { + if (this->check_incoming_crc_()) { + if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && + this->read_buffer_[3] == 'K') { + this->state_ = STATE_IDLE; + return; + } + // crc ok + this->state_ = STATE_POLL_CHECKED; + return; + } else { + this->state_ = STATE_IDLE; + } + } + + if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == PIPSOLAR_READ_BUFFER_LENGTH) { + this->read_pos_ = 0; + this->empty_uart_buffer_(); + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + + // end of answer + if (byte == 0x0D) { + this->read_buffer_[this->read_pos_] = 0; + this->empty_uart_buffer_(); + if (this->state_ == STATE_POLL) { + this->state_ = STATE_POLL_COMPLETE; + } + if (this->state_ == STATE_COMMAND) { + this->state_ = STATE_COMMAND_COMPLETE; + } + } + } // available + } + if (this->state_ == STATE_COMMAND) { + if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { + // command timeout + const char *command = this->command_queue_[this->command_queue_position_].c_str(); + this->command_start_millis_ = millis(); + ESP_LOGD(TAG, "timeout command from queue: %s", command); + this->command_queue_[this->command_queue_position_] = std::string(""); + this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; + this->state_ = STATE_IDLE; + return; + } else { + } + } + if (this->state_ == STATE_POLL) { + if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { + // command timeout + ESP_LOGD(TAG, "timeout command to poll: %s", this->used_polling_commands_[this->last_polling_command_].command); + this->state_ = STATE_IDLE; + } else { + } + } +} + +uint8_t Pipsolar::check_incoming_length_(uint8_t length) { + if (this->read_pos_ - 3 == length) { + return 1; + } + return 0; +} + +uint8_t Pipsolar::check_incoming_crc_() { + uint16_t crc16; + crc16 = calc_crc_(read_buffer_, read_pos_ - 3); + ESP_LOGD(TAG, "checking crc on incoming message"); + if (((uint8_t)((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && + ((uint8_t)((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { + ESP_LOGD(TAG, "CRC OK"); + read_buffer_[read_pos_ - 1] = 0; + read_buffer_[read_pos_ - 2] = 0; + read_buffer_[read_pos_ - 3] = 0; + return 1; + } + ESP_LOGD(TAG, "CRC NOK expected: %X %X but got: %X %X", ((uint8_t)((crc16) >> 8)), ((uint8_t)((crc16) &0xff)), + read_buffer_[read_pos_ - 3], read_buffer_[read_pos_ - 2]); + return 0; +} + +// send next command used +uint8_t Pipsolar::send_next_command_() { + uint16_t crc16; + if (this->command_queue_[this->command_queue_position_].length() != 0) { + const char *command = this->command_queue_[this->command_queue_position_].c_str(); + uint8_t byte_command[16]; + uint8_t length = this->command_queue_[this->command_queue_position_].length(); + for (uint8_t i = 0; i < length; i++) { + byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i); + } + this->state_ = STATE_COMMAND; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = calc_crc_(byte_command, length); + this->write_str(command); + // checksum + this->write(((uint8_t)((crc16) >> 8))); // highbyte + this->write(((uint8_t)((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length); + return 1; + } + return 0; +} + +void Pipsolar::send_next_poll_() { + uint16_t crc16; + this->last_polling_command_ = (this->last_polling_command_ + 1) % 15; + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + this->last_polling_command_ = 0; + } + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + // no command specified + return; + } + this->state_ = STATE_POLL; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = calc_crc_(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + this->write_array(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + // checksum + this->write(((uint8_t)((crc16) >> 8))); // highbyte + this->write(((uint8_t)((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending polling command : %s with length %d", + this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); +} + +void Pipsolar::queue_command_(const char *command, byte length) { + uint8_t next_position = command_queue_position_; + for (uint8_t i = 0; i < COMMAND_QUEUE_LENGTH; i++) { + uint8_t testposition = (next_position + i) % COMMAND_QUEUE_LENGTH; + if (command_queue_[testposition].length() == 0) { + command_queue_[testposition] = command; + ESP_LOGD(TAG, "Command queued successfully: %s with length %u at position %d", command, + command_queue_[testposition].length(), testposition); + return; + } + } + ESP_LOGD(TAG, "Command queue full dropping command: %s", command); +} + +void Pipsolar::switch_command(const std::string &command) { + ESP_LOGD(TAG, "got command: %s", command.c_str()); + queue_command_(command.c_str(), command.length()); +} +void Pipsolar::dump_config() { + ESP_LOGCONFIG(TAG, "Pipsolar:"); + ESP_LOGCONFIG(TAG, "used commands:"); + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length != 0) { + ESP_LOGCONFIG(TAG, "%s", used_polling_command.command); + } + } +} +void Pipsolar::update() {} + +void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) { + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length == strlen(command)) { + uint8_t len = strlen(command); + if (memcmp(used_polling_command.command, command, len) == 0) { + return; + } + } + if (used_polling_command.length == 0) { + size_t length = strlen(command) + 1; + const char *beg = command; + const char *end = command + length; + used_polling_command.command = new uint8_t[length]; + size_t i = 0; + for (; beg != end; ++beg, ++i) { + used_polling_command.command[i] = (uint8_t)(*beg); + } + used_polling_command.errors = 0; + used_polling_command.identifier = polling_command; + used_polling_command.length = length - 1; + return; + } + } +} + +uint16_t Pipsolar::calc_crc_(uint8_t *msg, int n) { + // Initial value. xmodem uses 0xFFFF but this example + // requires an initial value of zero. + uint16_t x = 0; + while (n--) { + x = crc_xmodem_update_(x, (uint16_t) *msg++); + } + return (x); +} + +// See bottom of this page: http://www.nongnu.org/avr-libc/user-manual/group__util__crc.html +// Polynomial: x^16 + x^12 + x^5 + 1 (0x1021) +uint16_t Pipsolar::crc_xmodem_update_(uint16_t crc, uint8_t data) { + int i; + crc = crc ^ ((uint16_t) data << 8); + for (i = 0; i < 8; i++) { + if (crc & 0x8000) + crc = (crc << 1) ^ 0x1021; //(polynomial = 0x1021) + else + crc <<= 1; + } + return crc; +} + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h new file mode 100644 index 0000000000..508036c7de --- /dev/null +++ b/esphome/components/pipsolar/pipsolar.h @@ -0,0 +1,223 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/switch/switch.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { + +enum ENUMPollingCommand { + POLLING_QPIRI = 0, + POLLING_QPIGS = 1, + POLLING_QMOD = 2, + POLLING_QFLAG = 3, + POLLING_QPIWS = 4, + POLLING_QT = 5, + POLLING_QMN = 6, +}; +struct PollingCommand { + uint8_t *command; + uint8_t length = 0; + uint8_t errors; + ENUMPollingCommand identifier; +}; + +#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \ + protected: \ + value_type value_##name##_; \ + PIPSOLAR_ENTITY_(type, name, polling_command) + +#define PIPSOLAR_ENTITY_(type, name, polling_command) \ + protected: \ + type *name##_{}; /* NOLINT */ \ +\ + public: \ + void set_##name(type *name) { /* NOLINT */ \ + this->name##_ = name; \ + this->add_polling_command_(#polling_command, POLLING_##polling_command); \ + } + +#define PIPSOLAR_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type) +#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command) +#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type) +#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \ + PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type) +#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command) + +class Pipsolar : public uart::UARTDevice, public PollingComponent { + // QPIGS values + PIPSOLAR_SENSOR(grid_voltage, QPIGS, float) + PIPSOLAR_SENSOR(grid_frequency, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float) + PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int) + PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int) + PIPSOLAR_SENSOR(output_load_percent, QPIGS, int) + PIPSOLAR_SENSOR(bus_voltage, QPIGS, int) + PIPSOLAR_SENSOR(battery_voltage, QPIGS, float) + PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int) + PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int) + PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, int) + PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float) + PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float) + PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int) + PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale + PIPSOLAR_SENSOR(eeprom_version, QPIGS, int) + PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int) + PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int) + + // QPIRI values + PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float) + PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int) + PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int) + PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float) + PIPSOLAR_SENSOR(battery_type, QPIRI, int) + PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int) + PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int) + PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int) + PIPSOLAR_SENSOR(output_source_priority, QPIRI, int) + PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int) + PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int) + PIPSOLAR_SENSOR(machine_type, QPIRI, int) + PIPSOLAR_SENSOR(topology, QPIRI, int) + PIPSOLAR_SENSOR(output_mode, QPIRI, int) + PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float) + PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int) + PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int) + + // QMOD values + PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char) + + // QFLAG values + PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int) + PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int) + + // QPIWS values + PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int) + PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool) + PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool) + + PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) + PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI) + PIPSOLAR_TEXT_SENSOR(last_qmod, QMOD) + PIPSOLAR_TEXT_SENSOR(last_qflag, QFLAG) + PIPSOLAR_TEXT_SENSOR(last_qpiws, QPIWS) + PIPSOLAR_TEXT_SENSOR(last_qt, QT) + PIPSOLAR_TEXT_SENSOR(last_qmn, QMN) + + PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI) + PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI) + PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) + PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) + + void switch_command(const std::string &command); + void setup() override; + void loop() override; + void dump_config() override; + void update() override; + + protected: + static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length + static const size_t COMMAND_QUEUE_LENGTH = 10; + static const size_t COMMAND_TIMEOUT = 5000; + uint32_t last_poll_ = 0; + void add_polling_command_(const char *command, ENUMPollingCommand polling_command); + void empty_uart_buffer_(); + uint8_t check_incoming_crc_(); + uint8_t check_incoming_length_(uint8_t length); + uint16_t calc_crc_(uint8_t *msg, int n); + uint16_t crc_xmodem_update_(uint16_t crc, uint8_t data); + uint8_t send_next_command_(); + void send_next_poll_(); + void queue_command_(const char *command, byte length); + std::string command_queue_[COMMAND_QUEUE_LENGTH]; + uint8_t command_queue_position_ = 0; + uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + uint32_t command_start_millis_ = 0; + uint8_t state_; + enum State { + STATE_IDLE = 0, + STATE_POLL = 1, + STATE_COMMAND = 2, + STATE_POLL_COMPLETE = 3, + STATE_COMMAND_COMPLETE = 4, + STATE_POLL_CHECKED = 5, + STATE_POLL_DECODED = 6, + }; + + uint8_t last_polling_command_ = 0; + PollingCommand used_polling_commands_[15]; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py new file mode 100644 index 0000000000..5e4dd6c40c --- /dev/null +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -0,0 +1,220 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, + UNIT_VOLT, + UNIT_EMPTY, + UNIT_VOLT_AMPS, + UNIT_WATT, + CONF_BUS_VOLTAGE, + CONF_BATTERY_VOLTAGE, +) +from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID + +DEPENDENCIES = ["uart"] + +# QPIRI sensors +CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" +CONF_GRID_RATING_CURRENT = "grid_rating_current" +CONF_AC_OUTPUT_RATING_VOLTAGE = "ac_output_rating_voltage" +CONF_AC_OUTPUT_RATING_FREQUENCY = "ac_output_rating_frequency" +CONF_AC_OUTPUT_RATING_CURRENT = "ac_output_rating_current" +CONF_AC_OUTPUT_RATING_APPARENT_POWER = "ac_output_rating_apparent_power" +CONF_AC_OUTPUT_RATING_ACTIVE_POWER = "ac_output_rating_active_power" +CONF_BATTERY_RATING_VOLTAGE = "battery_rating_voltage" +CONF_BATTERY_RECHARGE_VOLTAGE = "battery_recharge_voltage" +CONF_BATTERY_UNDER_VOLTAGE = "battery_under_voltage" +CONF_BATTERY_BULK_VOLTAGE = "battery_bulk_voltage" +CONF_BATTERY_FLOAT_VOLTAGE = "battery_float_voltage" +CONF_BATTERY_TYPE = "battery_type" +CONF_CURRENT_MAX_AC_CHARGING_CURRENT = "current_max_ac_charging_current" +CONF_CURRENT_MAX_CHARGING_CURRENT = "current_max_charging_current" +CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" +CONF_OUTPUT_SOURCE_PRIORITY = "output_source_priority" +CONF_CHARGER_SOURCE_PRIORITY = "charger_source_priority" +CONF_PARALLEL_MAX_NUM = "parallel_max_num" +CONF_MACHINE_TYPE = "machine_type" +CONF_TOPOLOGY = "topology" +CONF_OUTPUT_MODE = "output_mode" +CONF_BATTERY_REDISCHARGE_VOLTAGE = "battery_redischarge_voltage" +CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" +CONF_PV_POWER_BALANCE = "pv_power_balance" + +CONF_GRID_VOLTAGE = "grid_voltage" +CONF_GRID_FREQUENCY = "grid_frequency" +CONF_AC_OUTPUT_VOLTAGE = "ac_output_voltage" +CONF_AC_OUTPUT_FREQUENCY = "ac_output_frequency" +CONF_AC_OUTPUT_APPARENT_POWER = "ac_output_apparent_power" +CONF_AC_OUTPUT_ACTIVE_POWER = "ac_output_active_power" +CONF_OUTPUT_LOAD_PERCENT = "output_load_percent" +CONF_BATTERY_CHARGING_CURRENT = "battery_charging_current" +CONF_BATTERY_CAPACITY_PERCENT = "battery_capacity_percent" +CONF_INVERTER_HEAT_SINK_TEMPERATURE = "inverter_heat_sink_temperature" +CONF_PV_INPUT_CURRENT_FOR_BATTERY = "pv_input_current_for_battery" +CONF_PV_INPUT_VOLTAGE = "pv_input_voltage" +CONF_BATTERY_VOLTAGE_SCC = "battery_voltage_scc" +CONF_BATTERY_DISCHARGE_CURRENT = "battery_discharge_current" +CONF_ADD_SBU_PRIORITY_VERSION = "add_sbu_priority_version" +CONF_CONFIGURATION_STATUS = "configuration_status" +CONF_SCC_FIRMWARE_VERSION = "scc_firmware_version" +CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON = "battery_voltage_offset_for_fans_on" +CONF_EEPROM_VERSION = "eeprom_version" +CONF_PV_CHARGING_POWER = "pv_charging_power" + +TYPES = { + CONF_GRID_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_GRID_RATING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_TYPE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_MACHINE_TYPE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_TOPOLOGY: sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY), + CONF_OUTPUT_MODE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_POWER_BALANCE: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_GRID_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_GRID_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + ), + CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), + CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_BUS_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( + UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE + ), + CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + ), + CONF_EEPROM_VERSION: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + CONF_PV_CHARGING_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + ), +} + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): schema for type, schema in TYPES.items()} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, _ in TYPES.items(): + if type in config: + conf = config[type] + sens = await sensor.new_sensor(conf) + cg.add(getattr(paren, f"set_{type}")(sens)) diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py new file mode 100644 index 0000000000..5ff33b10ff --- /dev/null +++ b/esphome/components/pipsolar/switch/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ICON_POWER, +) +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +DEPENDENCIES = ["uart"] + +CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility" +CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar" +CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery" +CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" +CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" +CONF_PV_POWER_BALANCE = "pv_power_balance" + +TYPES = { + CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), + CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), + CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), + CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), + CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), + CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), +} + +PipsolarSwitch = pipsolar_ns.class_("PipsolarSwitch", switch.Switch, cg.Component) + +PIPSWITCH_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PipsolarSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Pipsolar switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_POWER): switch.icon, + } +).extend(cv.COMPONENT_SCHEMA) + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + {cv.Optional(type): PIPSWITCH_SCHEMA for type in TYPES} +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type, (on, off) in TYPES.items(): + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await switch.register_switch(var, conf) + cg.add(getattr(paren, f"set_{type}_switch")(var)) + cg.add(var.set_parent(paren)) + cg.add(var.set_on_command(on)) + if off is not None: + cg.add(var.set_off_command(off)) diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.cpp b/esphome/components/pipsolar/switch/pipsolar_switch.cpp new file mode 100644 index 0000000000..7eaeac1c2d --- /dev/null +++ b/esphome/components/pipsolar/switch/pipsolar_switch.cpp @@ -0,0 +1,24 @@ +#include "pipsolar_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.switch"; + +void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); } +void PipsolarSwitch::write_state(bool state) { + if (state) { + if (this->on_command_.length() > 0) { + this->parent_->switch_command(this->on_command_); + } + } else { + if (this->off_command_.length() > 0) { + this->parent_->switch_command(this->off_command_); + } + } +} + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.h b/esphome/components/pipsolar/switch/pipsolar_switch.h new file mode 100644 index 0000000000..3fe4c7dfa1 --- /dev/null +++ b/esphome/components/pipsolar/switch/pipsolar_switch.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; +class PipsolarSwitch : public switch_::Switch, public Component { + public: + void set_parent(Pipsolar *parent) { this->parent_ = parent; }; + void set_on_command(std::string command) { this->on_command_ = std::move(command); }; + void set_off_command(std::string command) { this->off_command_ = std::move(command); }; + void dump_config() override; + + protected: + void write_state(bool state) override; + std::string on_command_; + std::string off_command_; + Pipsolar *parent_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/__init__.py b/esphome/components/pipsolar/text_sensor/__init__.py new file mode 100644 index 0000000000..fe6c4979f3 --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/__init__.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns + +DEPENDENCIES = ["uart"] + +CONF_DEVICE_MODE = "device_mode" +CONF_LAST_QPIGS = "last_qpigs" +CONF_LAST_QPIRI = "last_qpiri" +CONF_LAST_QMOD = "last_qmod" +CONF_LAST_QFLAG = "last_qflag" +CONF_LAST_QPIWS = "last_qpiws" +CONF_LAST_QT = "last_qt" +CONF_LAST_QMN = "last_qmn" + +PipsolarTextSensor = pipsolar_ns.class_( + "PipsolarTextSensor", text_sensor.TextSensor, cg.Component +) + +TYPES = [ + CONF_DEVICE_MODE, + CONF_LAST_QPIGS, + CONF_LAST_QPIRI, + CONF_LAST_QMOD, + CONF_LAST_QFLAG, + CONF_LAST_QPIWS, + CONF_LAST_QT, + CONF_LAST_QMN, +] + +CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( + { + cv.Optional(type): text_sensor.TEXT_SENSOR_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PipsolarTextSensor)} + ) + for type in TYPES + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) + + for type in TYPES: + if type in config: + conf = config[type] + var = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(var, conf) + await cg.register_component(var, conf) + cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp new file mode 100644 index 0000000000..ee1fe2d1d8 --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp @@ -0,0 +1,13 @@ +#include "pipsolar_textsensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace pipsolar { + +static const char *const TAG = "pipsolar.text_sensor"; + +void PipsolarTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Pipsolar TextSensor", this); } + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h new file mode 100644 index 0000000000..871f6d8dee --- /dev/null +++ b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../pipsolar.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace pipsolar { +class Pipsolar; +class PipsolarTextSensor : public Component, public text_sensor::TextSensor { + public: + void set_parent(Pipsolar *parent) { this->parent_ = parent; }; + void dump_config() override; + + protected: + Pipsolar *parent_; +}; + +} // namespace pipsolar +} // namespace esphome diff --git a/esphome/components/pm1006/__init__.py b/esphome/components/pm1006/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp new file mode 100644 index 0000000000..9bedb3cfc0 --- /dev/null +++ b/esphome/components/pm1006/pm1006.cpp @@ -0,0 +1,96 @@ +#include "pm1006.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pm1006 { + +static const char *const TAG = "pm1006"; + +static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B}; + +void PM1006Component::setup() { + // because this implementation is currently rx-only, there is nothing to setup +} + +void PM1006Component::dump_config() { + ESP_LOGCONFIG(TAG, "PM1006:"); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + this->check_uart_settings(9600); +} + +void PM1006Component::loop() { + while (this->available() != 0) { + this->read_byte(&this->data_[this->data_index_]); + auto check = this->check_byte_(); + if (!check.has_value()) { + // finished + this->parse_data_(); + this->data_index_ = 0; + } else if (!*check) { + // wrong data + ESP_LOGV(TAG, "Byte %i of received data frame is invalid.", this->data_index_); + this->data_index_ = 0; + } else { + // next byte + this->data_index_++; + } + } +} + +float PM1006Component::get_setup_priority() const { return setup_priority::DATA; } + +uint8_t PM1006Component::pm1006_checksum_(const uint8_t *command_data, uint8_t length) const { + uint8_t sum = 0; + for (uint8_t i = 0; i < length; i++) { + sum += command_data[i]; + } + return sum; +} + +optional PM1006Component::check_byte_() const { + uint8_t index = this->data_index_; + uint8_t byte = this->data_[index]; + + // index 0..2 are the fixed header + if (index < sizeof(PM1006_RESPONSE_HEADER)) { + return byte == PM1006_RESPONSE_HEADER[index]; + } + + // just some additional notes here: + // index 3..4 is unused + // index 5..6 is our PM2.5 reading (3..6 is called DF1-DF4 in the datasheet at + // http://www.jdscompany.co.kr/download.asp?gubun=07&filename=PM1006_LED_PARTICLE_SENSOR_MODULE_SPECIFICATIONS.pdf + // that datasheet goes on up to DF16, which is unused for PM1006 but used in PM1006K + // so this code should be trivially extensible to support that one later + if (index < (sizeof(PM1006_RESPONSE_HEADER) + 16)) + return true; + + // checksum + if (index == (sizeof(PM1006_RESPONSE_HEADER) + 16)) { + uint8_t checksum = pm1006_checksum_(this->data_, sizeof(PM1006_RESPONSE_HEADER) + 17); + if (checksum != 0) { + ESP_LOGW(TAG, "PM1006 checksum is wrong: %02x, expected zero", checksum); + return false; + } + return {}; + } + + return false; +} + +void PM1006Component::parse_data_() { + const int pm_2_5_concentration = this->get_16_bit_uint_(5); + + ESP_LOGD(TAG, "Got PM2.5 Concentration: %d µg/m³", pm_2_5_concentration); + + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); + } +} + +uint16_t PM1006Component::get_16_bit_uint_(uint8_t start_index) const { + return encode_uint16(this->data_[start_index], this->data_[start_index + 1]); +} + +} // namespace pm1006 +} // namespace esphome diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h new file mode 100644 index 0000000000..66f4cf0311 --- /dev/null +++ b/esphome/components/pm1006/pm1006.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace pm1006 { + +class PM1006Component : public Component, public uart::UARTDevice { + public: + PM1006Component() = default; + + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { this->pm_2_5_sensor_ = pm_2_5_sensor; } + void setup() override; + void dump_config() override; + void loop() override; + + float get_setup_priority() const override; + + protected: + optional check_byte_() const; + void parse_data_(); + uint16_t get_16_bit_uint_(uint8_t start_index) const; + uint8_t pm1006_checksum_(const uint8_t *command_data, uint8_t length) const; + + sensor::Sensor *pm_2_5_sensor_{nullptr}; + + uint8_t data_[20]; + uint8_t data_index_{0}; + uint32_t last_transmission_{0}; +}; + +} // namespace pm1006 +} // namespace esphome diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py new file mode 100644 index 0000000000..8ea0e303f3 --- /dev/null +++ b/esphome/components/pm1006/sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_PM_2_5, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_BLUR, +) + +DEPENDENCIES = ["uart"] + +pm1006_ns = cg.esphome_ns.namespace("pm1006") +PM1006Component = pm1006_ns.class_("PM1006Component", uart.UARTDevice, cg.Component) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PM1006Component), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_BLUR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .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_PM_2_5 in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) diff --git a/esphome/components/pmsa003i/__init__.py b/esphome/components/pmsa003i/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp new file mode 100644 index 0000000000..1396c9f3d4 --- /dev/null +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -0,0 +1,100 @@ +#include "pmsa003i.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pmsa003i { + +static const char *const TAG = "pmsa003i"; + +void PMSA003IComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up pmsa003i..."); + + PM25AQIData data; + bool successful_read = this->read_data_(&data); + + if (!successful_read) { + this->mark_failed(); + return; + } +} + +void PMSA003IComponent::dump_config() { LOG_I2C_DEVICE(this); } + +void PMSA003IComponent::update() { + PM25AQIData data; + + bool successful_read = this->read_data_(&data); + + // Update sensors + if (successful_read) { + this->status_clear_warning(); + ESP_LOGV(TAG, "Read success. Updating sensors."); + + if (this->standard_units_) { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(data.pm10_standard); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(data.pm25_standard); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(data.pm100_standard); + } else { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(data.pm10_env); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(data.pm25_env); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(data.pm100_env); + } + + if (this->pmc_0_3_sensor_ != nullptr) + this->pmc_0_3_sensor_->publish_state(data.particles_03um); + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(data.particles_05um); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(data.particles_10um); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(data.particles_25um); + if (this->pmc_5_0_sensor_ != nullptr) + this->pmc_5_0_sensor_->publish_state(data.particles_50um); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(data.particles_100um); + } else { + this->status_set_warning(); + ESP_LOGV(TAG, "Read failure. Skipping update."); + } +} + +bool PMSA003IComponent::read_data_(PM25AQIData *data) { + const uint8_t num_bytes = 32; + uint8_t buffer[num_bytes]; + + this->read_bytes_raw(buffer, num_bytes); + + // https://github.com/adafruit/Adafruit_PM25AQI + + // Check that start byte is correct! + if (buffer[0] != 0x42) { + return false; + } + + // get checksum ready + int16_t sum = 0; + for (uint8_t i = 0; i < 30; i++) { + sum += buffer[i]; + } + + // The data comes in endian'd, this solves it so it works on all platforms + uint16_t buffer_u16[15]; + for (uint8_t i = 0; i < 15; i++) { + buffer_u16[i] = buffer[2 + i * 2 + 1]; + buffer_u16[i] += (buffer[2 + i * 2] << 8); + } + + // put it into a nice struct :) + memcpy((void *) data, (void *) buffer_u16, 30); + + return (sum == data->checksum); +} + +} // namespace pmsa003i +} // namespace esphome diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h new file mode 100644 index 0000000000..10176218ed --- /dev/null +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pmsa003i { + +/**! Structure holding Plantower's standard packet **/ +// From https://github.com/adafruit/Adafruit_PM25AQI +struct PM25AQIData { + uint16_t framelen; ///< How long this data chunk is + uint16_t pm10_standard, ///< Standard PM1.0 + pm25_standard, ///< Standard PM2.5 + pm100_standard; ///< Standard PM10.0 + uint16_t pm10_env, ///< Environmental PM1.0 + pm25_env, ///< Environmental PM2.5 + pm100_env; ///< Environmental PM10.0 + uint16_t particles_03um, ///< 0.3um Particle Count + particles_05um, ///< 0.5um Particle Count + particles_10um, ///< 1.0um Particle Count + particles_25um, ///< 2.5um Particle Count + particles_50um, ///< 5.0um Particle Count + particles_100um; ///< 10.0um Particle Count + uint16_t unused; ///< Unused + uint16_t checksum; ///< Packet checksum +}; + +class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_standard_units(bool standard_units) { standard_units_ = standard_units; } + + 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_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + + void set_pmc_0_3_sensor(sensor::Sensor *pmc_0_3) { pmc_0_3_sensor_ = pmc_0_3; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_5_0_sensor(sensor::Sensor *pmc_5_0) { pmc_5_0_sensor_ = pmc_5_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + protected: + bool read_data_(PM25AQIData *data); + + bool standard_units_; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + sensor::Sensor *pmc_0_3_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_5_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; +}; + +} // namespace pmsa003i +} // namespace esphome diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py new file mode 100644 index 0000000000..ac26270cfc --- /dev/null +++ b/esphome/components/pmsa003i/sensor.py @@ -0,0 +1,104 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_PMC_0_5, + CONF_PMC_1_0, + CONF_PMC_2_5, + CONF_PMC_10_0, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + ICON_COUNTER, + DEVICE_CLASS_EMPTY, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +pmsa003i_ns = cg.esphome_ns.namespace("pmsa003i") + +PMSA003IComponent = pmsa003i_ns.class_( + "PMSA003IComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONF_STANDARD_UNITS = "standard_units" +UNIT_COUNTS_PER_100ML = "#/0.1L" +CONF_PMC_0_3 = "pmc_0_3" +CONF_PMC_5_0 = "pmc_5_0" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PMSA003IComponent), + cv.Optional(CONF_STANDARD_UNITS, default=True): cv.boolean, + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 2, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 2, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 2, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x12)) +) + +TYPES = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_PMC_0_3: "set_pmc_0_3_sensor", + CONF_PMC_0_5: "set_pmc_0_5_sensor", + CONF_PMC_1_0: "set_pmc_1_0_sensor", + CONF_PMC_2_5: "set_pmc_2_5_sensor", + CONF_PMC_5_0: "set_pmc_5_0_sensor", + CONF_PMC_10_0: "set_pmc_10_0_sensor", +} + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_standard_units(config[CONF_STANDARD_UNITS])) + + for key, funcName in TYPES.items(): + + if key in config: + sens = yield sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index abea287c0b..0474d6ffd0 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -6,9 +6,39 @@ namespace pmsx003 { static const char *const TAG = "pmsx003"; +void PMSX003Component::set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { + pm_1_0_std_sensor_ = pm_1_0_std_sensor; +} +void PMSX003Component::set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { + pm_2_5_std_sensor_ = pm_2_5_std_sensor; +} +void PMSX003Component::set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor) { + pm_10_0_std_sensor_ = pm_10_0_std_sensor; +} + void PMSX003Component::set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void PMSX003Component::set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void PMSX003Component::set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + +void PMSX003Component::set_pm_particles_03um_sensor(sensor::Sensor *pm_particles_03um_sensor) { + pm_particles_03um_sensor_ = pm_particles_03um_sensor; +} +void PMSX003Component::set_pm_particles_05um_sensor(sensor::Sensor *pm_particles_05um_sensor) { + pm_particles_05um_sensor_ = pm_particles_05um_sensor; +} +void PMSX003Component::set_pm_particles_10um_sensor(sensor::Sensor *pm_particles_10um_sensor) { + pm_particles_10um_sensor_ = pm_particles_10um_sensor; +} +void PMSX003Component::set_pm_particles_25um_sensor(sensor::Sensor *pm_particles_25um_sensor) { + pm_particles_25um_sensor_ = pm_particles_25um_sensor; +} +void PMSX003Component::set_pm_particles_50um_sensor(sensor::Sensor *pm_particles_50um_sensor) { + pm_particles_50um_sensor_ = pm_particles_50um_sensor; +} +void PMSX003Component::set_pm_particles_100um_sensor(sensor::Sensor *pm_particles_100um_sensor) { + pm_particles_100um_sensor_ = pm_particles_100um_sensor; +} + void PMSX003Component::set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } @@ -102,19 +132,68 @@ optional PMSX003Component::check_byte_() { void PMSX003Component::parse_data_() { switch (this->type_) { + case PMSX003_TYPE_5003ST: { + uint16_t formaldehyde = this->get_16_bit_uint_(28); + float temperature = this->get_16_bit_uint_(30) / 10.0f; + float humidity = this->get_16_bit_uint_(32) / 10.0f; + + ESP_LOGD(TAG, "Got Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3", temperature, humidity, + formaldehyde); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->formaldehyde_sensor_ != nullptr) + this->formaldehyde_sensor_->publish_state(formaldehyde); + // The rest of the PMS5003ST matches the PMS5003, continue on + } case PMSX003_TYPE_X003: { + uint16_t pm_1_0_std_concentration = this->get_16_bit_uint_(4); + uint16_t pm_2_5_std_concentration = this->get_16_bit_uint_(6); + uint16_t pm_10_0_std_concentration = this->get_16_bit_uint_(8); + uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10); uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12); uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14); + + uint16_t pm_particles_03um = this->get_16_bit_uint_(16); + uint16_t pm_particles_05um = this->get_16_bit_uint_(18); + uint16_t pm_particles_10um = this->get_16_bit_uint_(20); + uint16_t pm_particles_25um = this->get_16_bit_uint_(22); + uint16_t pm_particles_50um = this->get_16_bit_uint_(24); + uint16_t pm_particles_100um = this->get_16_bit_uint_(26); + ESP_LOGD(TAG, "Got PM1.0 Concentration: %u µg/m^3, PM2.5 Concentration %u µg/m^3, PM10.0 Concentration: %u µg/m^3", pm_1_0_concentration, pm_2_5_concentration, pm_10_0_concentration); + + if (this->pm_1_0_std_sensor_ != nullptr) + this->pm_1_0_std_sensor_->publish_state(pm_1_0_std_concentration); + if (this->pm_2_5_std_sensor_ != nullptr) + this->pm_2_5_std_sensor_->publish_state(pm_2_5_std_concentration); + if (this->pm_10_0_std_sensor_ != nullptr) + this->pm_10_0_std_sensor_->publish_state(pm_10_0_std_concentration); + if (this->pm_1_0_sensor_ != nullptr) this->pm_1_0_sensor_->publish_state(pm_1_0_concentration); if (this->pm_2_5_sensor_ != nullptr) this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); if (this->pm_10_0_sensor_ != nullptr) this->pm_10_0_sensor_->publish_state(pm_10_0_concentration); + + if (this->pm_particles_03um_sensor_ != nullptr) + this->pm_particles_03um_sensor_->publish_state(pm_particles_03um); + if (this->pm_particles_05um_sensor_ != nullptr) + this->pm_particles_05um_sensor_->publish_state(pm_particles_05um); + if (this->pm_particles_10um_sensor_ != nullptr) + this->pm_particles_10um_sensor_->publish_state(pm_particles_10um); + if (this->pm_particles_25um_sensor_ != nullptr) + this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); + if (this->pm_particles_50um_sensor_ != nullptr) + this->pm_particles_50um_sensor_->publish_state(pm_particles_50um); + if (this->pm_particles_100um_sensor_ != nullptr) + this->pm_particles_100um_sensor_->publish_state(pm_particles_100um); break; } case PMSX003_TYPE_5003T: { @@ -131,29 +210,6 @@ void PMSX003Component::parse_data_() { this->humidity_sensor_->publish_state(humidity); break; } - case PMSX003_TYPE_5003ST: { - uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10); - uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12); - uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14); - uint16_t formaldehyde = this->get_16_bit_uint_(28); - float temperature = this->get_16_bit_uint_(30) / 10.0f; - float humidity = this->get_16_bit_uint_(32) / 10.0f; - ESP_LOGD(TAG, "Got PM2.5 Concentration: %u µg/m^3, Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3", - pm_2_5_concentration, temperature, humidity, formaldehyde); - if (this->pm_1_0_sensor_ != nullptr) - this->pm_1_0_sensor_->publish_state(pm_1_0_concentration); - if (this->pm_2_5_sensor_ != nullptr) - this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); - if (this->pm_10_0_sensor_ != nullptr) - this->pm_10_0_sensor_->publish_state(pm_10_0_concentration); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - if (this->formaldehyde_sensor_ != nullptr) - this->formaldehyde_sensor_->publish_state(formaldehyde); - break; - } } this->status_clear_warning(); @@ -163,9 +219,21 @@ uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { } void PMSX003Component::dump_config() { ESP_LOGCONFIG(TAG, "PMSX003:"); + LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_); + LOG_SENSOR(" ", "PM2.5STD", this->pm_2_5_std_sensor_); + LOG_SENSOR(" ", "PM10.0STD", this->pm_10_0_std_sensor_); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + + LOG_SENSOR(" ", "PM0.3um", this->pm_particles_03um_sensor_); + LOG_SENSOR(" ", "PM0.5um", this->pm_particles_05um_sensor_); + LOG_SENSOR(" ", "PM1.0um", this->pm_particles_10um_sensor_); + LOG_SENSOR(" ", "PM2.5um", this->pm_particles_25um_sensor_); + LOG_SENSOR(" ", "PM5.0um", this->pm_particles_50um_sensor_); + LOG_SENSOR(" ", "PM10.0um", this->pm_particles_100um_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index 163d25c694..a5adecb534 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -21,9 +21,22 @@ class PMSX003Component : public uart::UARTDevice, public Component { void dump_config() override; void set_type(PMSX003Type type) { type_ = type; } + + 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_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor); void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor); void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor); + + void set_pm_particles_03um_sensor(sensor::Sensor *pm_particles_03um_sensor); + void set_pm_particles_05um_sensor(sensor::Sensor *pm_particles_05um_sensor); + void set_pm_particles_10um_sensor(sensor::Sensor *pm_particles_10um_sensor); + void set_pm_particles_25um_sensor(sensor::Sensor *pm_particles_25um_sensor); + void set_pm_particles_50um_sensor(sensor::Sensor *pm_particles_50um_sensor); + void set_pm_particles_100um_sensor(sensor::Sensor *pm_particles_100um_sensor); + void set_temperature_sensor(sensor::Sensor *temperature_sensor); void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor); @@ -37,9 +50,25 @@ class PMSX003Component : public uart::UARTDevice, public Component { uint8_t data_index_{0}; uint32_t last_transmission_{0}; PMSX003Type type_; + + // "Standard Particle" + sensor::Sensor *pm_1_0_std_sensor_{nullptr}; + sensor::Sensor *pm_2_5_std_sensor_{nullptr}; + sensor::Sensor *pm_10_0_std_sensor_{nullptr}; + + // "Under Atmospheric Pressure" sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + + // Particle counts by size + sensor::Sensor *pm_particles_03um_sensor_{nullptr}; + sensor::Sensor *pm_particles_05um_sensor_{nullptr}; + sensor::Sensor *pm_particles_10um_sensor_{nullptr}; + sensor::Sensor *pm_particles_25um_sensor_{nullptr}; + sensor::Sensor *pm_particles_50um_sensor_{nullptr}; + sensor::Sensor *pm_particles_100um_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *formaldehyde_sensor_{nullptr}; diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 80f2b80e5e..c3dd7d5a97 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -8,16 +8,25 @@ from esphome.const import ( CONF_PM_10_0, CONF_PM_1_0, CONF_PM_2_5, + CONF_PM_10_0_STD, + CONF_PM_1_0_STD, + CONF_PM_2_5_STD, + CONF_PM_0_3UM, + CONF_PM_0_5UM, + CONF_PM_1_0UM, + CONF_PM_2_5UM, + CONF_PM_5_0UM, + CONF_PM_10_0UM, CONF_TEMPERATURE, CONF_TYPE, DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_CHEMICAL_WEAPON, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, + UNIT_COUNT_DECILITRE, UNIT_PERCENT, ) @@ -62,47 +71,95 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PMSX003Component), cv.Required(CONF_TYPE): cv.enum(PMSX003_TYPES, upper=True), - cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + cv.Optional(CONF_PM_1_0_STD): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, ICON_CHEMICAL_WEAPON, 0, DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -118,6 +175,18 @@ async def to_code(config): cg.add(var.set_type(config[CONF_TYPE])) + if CONF_PM_1_0_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0_STD]) + cg.add(var.set_pm_1_0_std_sensor(sens)) + + if CONF_PM_2_5_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5_STD]) + cg.add(var.set_pm_2_5_std_sensor(sens)) + + if CONF_PM_10_0_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0_STD]) + cg.add(var.set_pm_10_0_std_sensor(sens)) + if CONF_PM_1_0 in config: sens = await sensor.new_sensor(config[CONF_PM_1_0]) cg.add(var.set_pm_1_0_sensor(sens)) @@ -130,6 +199,30 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_PM_0_3UM in config: + sens = await sensor.new_sensor(config[CONF_PM_0_3UM]) + cg.add(var.set_pm_particles_03um_sensor(sens)) + + if CONF_PM_0_5UM in config: + sens = await sensor.new_sensor(config[CONF_PM_0_5UM]) + cg.add(var.set_pm_particles_05um_sensor(sens)) + + if CONF_PM_1_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0UM]) + cg.add(var.set_pm_particles_10um_sensor(sens)) + + if CONF_PM_2_5UM in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5UM]) + cg.add(var.set_pm_particles_25um_sensor(sens)) + + if CONF_PM_5_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_5_0UM]) + cg.add(var.set_pm_particles_50um_sensor(sens)) + + if CONF_PM_10_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0UM]) + cg.add(var.set_pm_particles_100um_sensor(sens)) + if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 71227ec491..767728fc80 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( CONF_RISING_EDGE, CONF_NUMBER, CONF_TOTAL, - DEVICE_CLASS_EMPTY, ICON_PULSE, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -67,11 +66,10 @@ def validate_count_mode(value): CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PULSES_PER_MINUTE, - ICON_PULSE, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -94,7 +92,10 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 4b8e8a1be0..18da842bad 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -15,7 +15,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PULSES, UNIT_PULSES_PER_MINUTE, - DEVICE_CLASS_EMPTY, ) from esphome.core import CORE @@ -51,7 +50,10 @@ def validate_pulse_meter_pin(value): CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.GenerateID(): cv.declare_id(PulseMeterSensor), @@ -59,12 +61,11 @@ CONFIG_SCHEMA = sensor.sensor_schema( cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TIMEOUT, default="5min"): validate_timeout, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, - ICON_PULSE, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/pulse_width/sensor.py b/esphome/components/pulse_width/sensor.py index 6a6147c6aa..6c91104036 100644 --- a/esphome/components/pulse_width/sensor.py +++ b/esphome/components/pulse_width/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_PIN, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_SECOND, ICON_TIMER, @@ -19,7 +18,10 @@ PulseWidthSensor = pulse_width_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_SECOND, ICON_TIMER, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/pvvx_mithermometer/__init__.py b/esphome/components/pvvx_mithermometer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp new file mode 100644 index 0000000000..34f190e45e --- /dev/null +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -0,0 +1,145 @@ +#include "pvvx_mithermometer.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace pvvx_mithermometer { + +static const char *TAG = "pvvx_mithermometer"; + +void PVVXMiThermometer::dump_config() { + ESP_LOGCONFIG(TAG, "PVVX MiThermometer"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); +} + +bool PVVXMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = parse_header(service_data); + if (res->is_duplicate) { + continue; + } + if (!(parse_message(service_data.data, *res))) { + continue; + } + if (!(report_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->battery_voltage.has_value() && this->battery_voltage_ != nullptr) + this->battery_voltage_->publish_state(*res->battery_voltage); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +optional PVVXMiThermometer::parse_header(const esp32_ble_tracker::ServiceData &service_data) { + ParseResult result; + if (!service_data.uuid.contains(0x1A, 0x18)) { + ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); + return {}; + } + + auto raw = service_data.data; + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); + result.is_duplicate = true; + return {}; + } + last_frame_count = raw[13]; + result.is_duplicate = false; + + return result; +} + +bool PVVXMiThermometer::parse_message(const std::vector &message, ParseResult &result) { + /* + All data little endian + uint8_t size; // = 19 + uint8_t uid; // = 0x16, 16-bit UUID + uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - lo, .. [5] - hi digits + int16_t temperature; // x 0.01 degree [6,7] + uint16_t humidity; // x 0.01 % [8,9] + uint16_t battery_mv; // mV [10,11] + uint8_t battery_level; // 0..100 % [12] + uint8_t counter; // measurement count [13] + uint8_t flags; [14] + */ + + const uint8_t *data = message.data(); + const int data_length = 15; + + if (message.size() != data_length) { + ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); + return false; + } + + // int16_t temperature; // x 0.01 degree [6,7] + const int16_t temperature = int16_t(data[6]) | (int16_t(data[7]) << 8); + result.temperature = temperature / 1.0e2f; + + // uint16_t humidity; // x 0.01 % [8,9] + const int16_t humidity = uint16_t(data[8]) | (uint16_t(data[9]) << 8); + result.humidity = humidity / 1.0e2f; + + // uint16_t battery_mv; // mV [10,11] + const int16_t battery_voltage = uint16_t(data[10]) | (uint16_t(data[11]) << 8); + result.battery_voltage = battery_voltage / 1.0e3f; + + // uint8_t battery_level; // 0..100 % [12] + result.battery_level = uint8_t(data[12]); + + return true; +} + +bool PVVXMiThermometer::report_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got PVVX MiThermometer (%s):", address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.2f °C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.2f %%", *result->humidity); + } + if (result->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f %%", *result->battery_level); + } + if (result->battery_voltage.has_value()) { + ESP_LOGD(TAG, " Battery Voltage: %.3f V", *result->battery_voltage); + } + + return true; +} + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h new file mode 100644 index 0000000000..42a40d4200 --- /dev/null +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace pvvx_mithermometer { + +struct ParseResult { + optional temperature; + optional humidity; + optional battery_level; + optional battery_voltage; + bool is_duplicate; + int raw_offset; +}; + +class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + + optional parse_header(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message(const std::vector &message, ParseResult &result); + bool report_results(const optional &result, const std::string &address); +}; + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py new file mode 100644 index 0000000000..b17878f01b --- /dev/null +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -0,0 +1,84 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_MAC_ADDRESS, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_ID, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@pasiz"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +pvvx_mithermometer_ns = cg.esphome_ns.namespace("pvvx_mithermometer") +PVVXMiThermometer = pvvx_mithermometer_ns.class_( + "PVVXMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PVVXMiThermometer), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_BATTERY_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + cg.add(var.set_battery_voltage(sens)) diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index b358b8c650..23502e849a 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -30,25 +29,29 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEM004T), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 1dd77a0371..1616bf0ace 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -9,13 +9,11 @@ from esphome.const import ( CONF_VOLTAGE, CONF_FREQUENCY, CONF_POWER_FACTOR, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY, - ICON_EMPTY, ICON_CURRENT_AC, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, @@ -23,7 +21,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - UNIT_EMPTY, UNIT_WATT_HOURS, ) @@ -37,39 +34,40 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEMAC), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER_FACTOR, - STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 58afea8e30..08ec688afb 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -26,17 +25,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEMDC), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 43e7939cf1..9e80cdbd88 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -116,7 +116,7 @@ void QMC5883LComponent::update() { bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { bool success = this->read_byte_16(a_register, data); - *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte oder, LSB first; + *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte order, LSB first; return success; } diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index d0fdf1b77a..27d1df5b29 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, - DEVICE_CLASS_EMPTY, ICON_MAGNET, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -71,10 +70,16 @@ def validate_enum(enum_values, units=None, int=True): field_strength_schema = sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) heading_schema = sensor.sensor_schema( - UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 789ab6197b..6261f63132 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -162,7 +162,7 @@ void RC522::loop() { ESP_LOGW(TAG, "CMD_REQA -> Not OK %d", status); state_ = STATE_DONE; } else if (back_length_ != 2) { // || *valid_bits_ != 0) { // ATQA must be exactly 16 bits. - ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpacted back_length_ of %d", back_length_); + ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpected back_length_ of %d", back_length_); state_ = STATE_DONE; } else { state_ = STATE_READ_SERIAL; @@ -470,7 +470,7 @@ RC522::StatusCode RC522::await_crc_() { return STATUS_WAITING; ESP_LOGD(TAG, "pcd_calculate_crc_() TIMEOUT"); - // 89ms passed and nothing happend. Communication with the MFRC522 might be down. + // 89ms passed and nothing happened. Communication with the MFRC522 might be down. return STATUS_TIMEOUT; } diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index 7fb49e97fd..6880651ac4 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -32,7 +32,7 @@ class RC522 : public PollingComponent { STATUS_OK, // Success STATUS_WAITING, // Waiting result from RC522 chip STATUS_ERROR, // Error in communication - STATUS_COLLISION, // Collission detected + STATUS_COLLISION, // Collision detected STATUS_TIMEOUT, // Timeout in communication. STATUS_NO_ROOM, // A buffer is not big enough. STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 7c27c1b736..c9f1c611a8 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -234,6 +234,49 @@ async def build_dumpers(config): return dumpers +# Dish +DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( + "Dish" +) +DISH_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ADDRESS, default=1): cv.int_range(min=1, max=16), + cv.Required(CONF_COMMAND): cv.int_range(min=0, max=63), + } +) + + +@register_binary_sensor("dish", DishBinarySensor, DISH_SCHEMA) +def dish_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DishData, + ("address", config[CONF_ADDRESS]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("dish", DishTrigger, DishData) +def dish_trigger(var, config): + pass + + +@register_dumper("dish", DishDumper) +def dish_dumper(var, config): + pass + + +@register_action("dish", DishAction, DISH_SCHEMA) +async def dish_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # JVC JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) @@ -950,6 +993,53 @@ async def samsung36_action(var, config, args): cg.add(var.set_command(template_)) +# Toshiba AC +( + ToshibaAcData, + ToshibaAcBinarySensor, + ToshibaAcTrigger, + ToshibaAcAction, + ToshibaAcDumper, +) = declare_protocol("ToshibaAc") +TOSHIBAAC_SCHEMA = cv.Schema( + { + cv.Required(CONF_RC_CODE_1): cv.hex_uint64_t, + cv.Optional(CONF_RC_CODE_2, default=0): cv.hex_uint64_t, + } +) + + +@register_binary_sensor("toshiba_ac", ToshibaAcBinarySensor, TOSHIBAAC_SCHEMA) +def toshibaac_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + ToshibaAcData, + ("rc_code_1", config[CONF_RC_CODE_1]), + ("rc_code_2", config[CONF_RC_CODE_2]), + ) + ) + ) + + +@register_trigger("toshiba_ac", ToshibaAcTrigger, ToshibaAcData) +def toshibaac_trigger(var, config): + pass + + +@register_dumper("toshiba_ac", ToshibaAcDumper) +def toshibaac_dumper(var, config): + pass + + +@register_action("toshiba_ac", ToshibaAcAction, TOSHIBAAC_SCHEMA) +async def toshibaac_action(var, config, args): + template_ = await cg.templatable(config[CONF_RC_CODE_1], args, cg.uint64) + cg.add(var.set_rc_code_1(template_)) + template_ = await cg.templatable(config[CONF_RC_CODE_2], args, cg.uint64) + cg.add(var.set_rc_code_2(template_)) + + # Panasonic ( PanasonicData, diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp new file mode 100644 index 0000000000..1257e22a45 --- /dev/null +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -0,0 +1,92 @@ +#include "dish_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.dish"; + +static const uint32_t HEADER_HIGH_US = 400; +static const uint32_t HEADER_LOW_US = 6100; +static const uint32_t BIT_HIGH_US = 400; +static const uint32_t BIT_ONE_LOW_US = 1700; +static const uint32_t BIT_ZERO_LOW_US = 2800; + +void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) { + dst->reserve(138); + dst->set_carrier_frequency(57600); + + // HEADER + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + + // Typically a DISH device needs to get a command a total of + // at least 4 times to accept it. + for (uint i = 0; i < 4; i++) { + // COMMAND (function, in MSB) + for (uint8_t mask = 1UL << 5; mask; mask >>= 1) { + if (data.command & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + // ADDRESS (unit code, in LSB) + for (uint8_t mask = 1UL; mask < 1UL << 4; mask <<= 1) { + if ((data.address - 1) & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + // PADDING + for (uint j = 0; j < 6; j++) + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + + // FOOTER + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + } +} +optional DishProtocol::decode(RemoteReceiveData src) { + DishData data{ + .address = 0, + .command = 0, + }; + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + for (uint8_t mask = 1UL << 5; mask != 0; mask >>= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + data.command |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + data.command &= ~mask; + } else { + return {}; + } + } + + for (uint8_t mask = 1UL; mask < 1UL << 5; mask <<= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + data.address |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + data.address &= ~mask; + } else { + return {}; + } + } + for (uint j = 0; j < 6; j++) { + if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + return {}; + } + } + data.address++; + + src.expect_item(HEADER_HIGH_US, HEADER_LOW_US); + + return data; +} + +void DishProtocol::dump(const DishData &data) { + ESP_LOGD(TAG, "Received Dish: address=0x%02X, command=0x%02X", data.address, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/dish_protocol.h b/esphome/components/remote_base/dish_protocol.h new file mode 100644 index 0000000000..ca4d04ed34 --- /dev/null +++ b/esphome/components/remote_base/dish_protocol.h @@ -0,0 +1,38 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct DishData { + uint8_t address; + uint8_t command; + + bool operator==(const DishData &rhs) const { return address == rhs.address && command == rhs.command; } +}; + +class DishProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DishData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DishData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Dish) + +template class DishAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint8_t, address) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DishData data{}; + data.address = this->address_.value(x...); + data.command = this->command_.value(x...); + DishProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 7198f2d917..d36c0d7ebe 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -16,7 +16,7 @@ RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_b void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_7) { this->mem_block_num_ = int(RMT_CHANNEL_7) - int(this->channel_) + 1; - ESP_LOGW(TAG, "Not enough RMT memory blocks avaiable, reduced to %i blocks.", this->mem_block_num_); + ESP_LOGW(TAG, "Not enough RMT memory blocks available, reduced to %i blocks.", this->mem_block_num_); } rmt.channel = this->channel_; rmt.clk_div = this->clock_divider_; diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp new file mode 100644 index 0000000000..27d042ace0 --- /dev/null +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -0,0 +1,111 @@ +#include "toshiba_ac_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.toshibaac"; + +static const uint32_t HEADER_HIGH_US = 4500; +static const uint32_t HEADER_LOW_US = 4500; +static const uint32_t BIT_HIGH_US = 560; +static const uint32_t BIT_ONE_LOW_US = 1690; +static const uint32_t BIT_ZERO_LOW_US = 560; +static const uint32_t FOOTER_HIGH_US = 560; +static const uint32_t FOOTER_LOW_US = 4500; +static const uint16_t PACKET_SPACE = 5500; + +void ToshibaAcProtocol::encode(RemoteTransmitData *dst, const ToshibaAcData &data) { + dst->set_carrier_frequency(38000); + dst->reserve((3 + (48 * 2)) * 3); + + for (uint8_t repeat = 0; repeat < 2; repeat++) { + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint8_t bit = 48; bit > 0; bit--) { + dst->mark(BIT_HIGH_US); + if ((data.rc_code_1 >> (bit - 1)) & 1) + dst->space(BIT_ONE_LOW_US); + else + dst->space(BIT_ZERO_LOW_US); + } + dst->item(FOOTER_HIGH_US, FOOTER_LOW_US); + } + + if (data.rc_code_2 != 0) { + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint8_t bit = 48; bit > 0; bit--) { + dst->mark(BIT_HIGH_US); + if ((data.rc_code_2 >> (bit - 1)) & 1) + dst->space(BIT_ONE_LOW_US); + else + dst->space(BIT_ZERO_LOW_US); + } + dst->item(FOOTER_HIGH_US, FOOTER_LOW_US); + } +} + +optional ToshibaAcProtocol::decode(RemoteReceiveData src) { + uint64_t packet = 0; + ToshibaAcData out{ + .rc_code_1 = 0, + .rc_code_2 = 0, + }; + // *** Packet 1 + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + for (uint8_t bit_counter = 0; bit_counter < 48; bit_counter++) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + packet = (packet << 1) | 1; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + packet = (packet << 1) | 0; + } else { + return {}; + } + } + if (!src.expect_item(FOOTER_HIGH_US, PACKET_SPACE)) + return {}; + + // *** Packet 2 + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + for (uint8_t bit_counter = 0; bit_counter < 48; bit_counter++) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.rc_code_1 = (out.rc_code_1 << 1) | 1; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + out.rc_code_1 = (out.rc_code_1 << 1) | 0; + } else { + return {}; + } + } + // The first two packets must match + if (packet != out.rc_code_1) + return {}; + // The third packet isn't always present + if (!src.expect_item(FOOTER_HIGH_US, PACKET_SPACE)) + return out; + + // *** Packet 3 + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + for (uint8_t bit_counter = 0; bit_counter < 48; bit_counter++) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + out.rc_code_2 = (out.rc_code_2 << 1) | 1; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + out.rc_code_2 = (out.rc_code_2 << 1) | 0; + } else { + return {}; + } + } + + return out; +} + +void ToshibaAcProtocol::dump(const ToshibaAcData &data) { + if (data.rc_code_2 != 0) + ESP_LOGD(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64 ", rc_code_2=0x%" PRIX64, data.rc_code_1, data.rc_code_2); + else + ESP_LOGD(TAG, "Received Toshiba AC: rc_code_1=0x%" PRIX64, data.rc_code_1); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/toshiba_ac_protocol.h b/esphome/components/remote_base/toshiba_ac_protocol.h new file mode 100644 index 0000000000..c69401c378 --- /dev/null +++ b/esphome/components/remote_base/toshiba_ac_protocol.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct ToshibaAcData { + uint64_t rc_code_1; + uint64_t rc_code_2; + + bool operator==(const ToshibaAcData &rhs) const { return rc_code_1 == rhs.rc_code_1 && rc_code_2 == rhs.rc_code_2; } +}; + +class ToshibaAcProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const ToshibaAcData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const ToshibaAcData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(ToshibaAc) + +template class ToshibaAcAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint64_t, rc_code_1) + TEMPLATABLE_VALUE(uint64_t, rc_code_2) + + void encode(RemoteTransmitData *dst, Ts... x) override { + ToshibaAcData data{}; + data.rc_code_1 = this->rc_code_1_.value(x...); + data.rc_code_2 = this->rc_code_2_.value(x...); + ToshibaAcProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index ca1501195a..329192e902 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_SENSOR, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_OHM, ICON_FLASH, @@ -25,7 +24,10 @@ CONFIGURATIONS = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_OHM, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index 1a3bf9f614..ef53c8042d 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -15,8 +15,7 @@ class RGBLightOutput : public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbct/__init__.py b/esphome/components/rgbct/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/rgbct/light.py b/esphome/components/rgbct/light.py new file mode 100644 index 0000000000..e525c207c7 --- /dev/null +++ b/esphome/components/rgbct/light.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light, output +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_TEMPERATURE, + CONF_GREEN, + CONF_RED, + CONF_OUTPUT_ID, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, +) + +CODEOWNERS = ["@jesserockz"] + +rgbct_ns = cg.esphome_ns.namespace("rgbct") +RGBCTLightOutput = rgbct_ns.class_("RGBCTLightOutput", light.LightOutput) + +CONF_COLOR_INTERLOCK = "color_interlock" +CONF_WHITE_BRIGHTNESS = "white_brightness" + +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBCTLightOutput), + cv.Required(CONF_RED): cv.use_id(output.FloatOutput), + cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), + cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLOR_TEMPERATURE): cv.use_id(output.FloatOutput), + cv.Required(CONF_WHITE_BRIGHTNESS): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, + } + ), + light.validate_color_temperature_channels, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + red = await cg.get_variable(config[CONF_RED]) + cg.add(var.set_red(red)) + green = await cg.get_variable(config[CONF_GREEN]) + cg.add(var.set_green(green)) + blue = await cg.get_variable(config[CONF_BLUE]) + cg.add(var.set_blue(blue)) + + color_temp = await cg.get_variable(config[CONF_COLOR_TEMPERATURE]) + cg.add(var.set_color_temperature(color_temp)) + white_brightness = await cg.get_variable(config[CONF_WHITE_BRIGHTNESS]) + cg.add(var.set_white_brightness(white_brightness)) + + cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbct/rgbct_light_output.h b/esphome/components/rgbct/rgbct_light_output.h new file mode 100644 index 0000000000..9257d67cd1 --- /dev/null +++ b/esphome/components/rgbct/rgbct_light_output.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/components/light/color_mode.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace rgbct { + +class RGBCTLightOutput : public light::LightOutput { + public: + void set_red(output::FloatOutput *red) { red_ = red; } + void set_green(output::FloatOutput *green) { green_ = green; } + void set_blue(output::FloatOutput *blue) { blue_ = blue; } + + void set_color_temperature(output::FloatOutput *color_temperature) { color_temperature_ = color_temperature; } + void set_white_brightness(output::FloatOutput *white_brightness) { white_brightness_ = white_brightness; } + + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); + traits.set_min_mireds(this->cold_white_temperature_); + traits.set_max_mireds(this->warm_white_temperature_); + return traits; + } + void write_state(light::LightState *state) override { + float red, green, blue, color_temperature, white_brightness; + + state->current_values_as_rgbct(&red, &green, &blue, &color_temperature, &white_brightness); + + this->red_->set_level(red); + this->green_->set_level(green); + this->blue_->set_level(blue); + this->color_temperature_->set_level(color_temperature); + this->white_brightness_->set_level(white_brightness); + } + + protected: + output::FloatOutput *red_; + output::FloatOutput *green_; + output::FloatOutput *blue_; + output::FloatOutput *color_temperature_; + output::FloatOutput *white_brightness_; + float cold_white_temperature_; + float warm_white_temperature_; + bool color_interlock_{true}; +}; + +} // namespace rgbct +} // namespace esphome diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index 90a650851b..0f55775608 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -16,10 +16,10 @@ class RGBWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_color_interlock(this->color_interlock_); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index 41cba1f7a1..c0ce85e267 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import light, output from esphome.const import ( CONF_BLUE, + CONF_CONSTANT_BRIGHTNESS, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, @@ -15,7 +16,6 @@ from esphome.const import ( rgbww_ns = cg.esphome_ns.namespace("rgbww") RGBWWLightOutput = rgbww_ns.class_("RGBWWLightOutput", light.LightOutput) -CONF_CONSTANT_BRIGHTNESS = "constant_brightness" CONF_COLOR_INTERLOCK = "color_interlock" CONFIG_SCHEMA = cv.All( @@ -27,12 +27,15 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, } ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), light.validate_color_temperature_channels, ) @@ -50,10 +53,17 @@ async def to_code(config): cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index e14b967530..5a86b88595 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -14,25 +14,23 @@ class RGBWWLightOutput : public light::LightOutput { void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_cold_white(output::FloatOutput *cold_white) { cold_white_ = cold_white; } void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } - void set_cold_white_temperature(int cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } - void set_warm_white_temperature(int warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_temperature(true); - traits.set_supports_color_interlock(this->color_interlock_); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; } void write_state(light::LightState *state) override { float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_, - this->color_interlock_); + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -46,8 +44,8 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *blue_; output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - int cold_white_temperature_; - int warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; bool color_interlock_{false}; }; diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 079f00d284..311e63a7ba 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_STEPS, ICON_ROTATE_RIGHT, @@ -58,7 +57,10 @@ def validate_min_max_value(config): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_STEPS, ICON_ROTATE_RIGHT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_STEPS, + icon=ICON_ROTATE_RIGHT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py index 12b8425d14..342a5eff24 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -14,13 +14,11 @@ from esphome.const import ( CONF_TX_POWER, CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_CELSIUS, @@ -29,7 +27,6 @@ from esphome.const import ( UNIT_HECTOPASCAL, UNIT_G, UNIT_DECIBEL_MILLIWATT, - UNIT_EMPTY, ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, @@ -52,69 +49,68 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(RuuviTag), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 2, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_X, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_X, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_Y, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_Y, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_Z, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_Z, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TX_POWER): sensor.sensor_schema( - UNIT_DECIBEL_MILLIWATT, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema( - UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3eda98d41d..838c92f04e 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -55,7 +55,7 @@ void SCD30Component::setup() { // According ESP32 clock stretching is typically 30ms and up to 150ms "due to // internal calibration processes". The I2C peripheral only supports 13ms (at // least when running at 80MHz). - // In practise it seems that clock stretching occures during this calibration + // In practise it seems that clock stretching occurs during this calibration // calls. It also seems that delays in between calls makes them // disappear/shorter. Hence work around with delays for ESP32. // diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 7a08289474..c0317c96e0 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,13 +3,11 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, CONF_HUMIDITY, CONF_TEMPERATURE, CONF_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, @@ -33,25 +31,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SCD30Component), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All( diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 43356c0036..9702878475 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -50,7 +50,7 @@ def assign_declare_id(value): CONFIG_SCHEMA = automation.validate_automation( { # Don't declare id as cv.declare_id yet, because the ID type - # dpeends on the mode. Will be checked later with assign_declare_id + # depends on the mode. Will be checked later with assign_declare_id cv.Required(CONF_ID): cv.string_strict, cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of( *SCRIPT_MODES, lower=True diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 64db6b80e7..5663d32ce8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -65,7 +65,7 @@ class QueueingScript : public Script, public Component { /** A script type that executes new instances in parallel. * * If a new instance is started while previous ones haven't finished yet, - * the new one is exeucted in parallel to the other instances. + * the new one is executed in parallel to the other instances. */ class ParallelScript : public Script { public: diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index ce560b9d4b..13cc94786c 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -17,19 +17,16 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, ICON_FLASH, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_DEGREES, - UNIT_EMPTY, UNIT_HERTZ, UNIT_VOLT, UNIT_VOLT_AMPS, @@ -46,27 +43,43 @@ sdm_meter_ns = cg.esphome_ns.namespace("sdm_meter") SDMMeter = sdm_meter_ns.class_("SDMMeter", cg.PollingComponent, modbus.ModbusDevice) PHASE_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_PHASE_ANGLE: sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, icon=ICON_FLASH, accuracy_decimals=3 ), - CONF_PHASE_ANGLE: sensor.sensor_schema(UNIT_DEGREES, ICON_FLASH, 3), } PHASE_SCHEMA = cv.Schema( @@ -81,43 +94,38 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/sdp3x/__init__.py b/esphome/components/sdp3x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp new file mode 100644 index 0000000000..5e6c5887ac --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -0,0 +1,124 @@ +#include "sdp3x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace sdp3x { + +static const char *const TAG = "sdp3x.sensor"; +static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06}; +static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C}; +static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02}; +static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15}; +static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9}; + +void SDP3XComponent::update() { this->read_pressure_(); } + +void SDP3XComponent::setup() { + ESP_LOGD(TAG, "Setting up SDP3X..."); + + if (!this->write_bytes_raw(SDP3X_STOP_MEAS, 2)) { + ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason + } + + if (!this->write_bytes_raw(SDP3X_SOFT_RESET, 2)) { + ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason + } + + delay_microseconds_accurate(20000); + + if (!this->write_bytes_raw(SDP3X_READ_ID1, 2)) { + ESP_LOGE(TAG, "Read ID1 SDP3X failed!"); + this->mark_failed(); + return; + } + if (!this->write_bytes_raw(SDP3X_READ_ID2, 2)) { + ESP_LOGE(TAG, "Read ID2 SDP3X failed!"); + this->mark_failed(); + return; + } + + uint8_t data[18]; + if (!this->read_bytes_raw(data, 18)) { + ESP_LOGE(TAG, "Read ID SDP3X failed!"); + this->mark_failed(); + return; + } + + if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) { + ESP_LOGE(TAG, "CRC ID SDP3X failed!"); + this->mark_failed(); + return; + } + + if (data[3] == 0x01) { + ESP_LOGCONFIG(TAG, "SDP3X is SDP31"); + pressure_scale_factor_ = 60.0f * 100.0f; // Scale factors converted to hPa per count + } else if (data[3] == 0x02) { + ESP_LOGCONFIG(TAG, "SDP3X is SDP32"); + pressure_scale_factor_ = 240.0f * 100.0f; + } + + if (!this->write_bytes_raw(SDP3X_START_DP_AVG, 2)) { + ESP_LOGE(TAG, "Start Measurements SDP3X failed!"); + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "SDP3X started!"); +} +void SDP3XComponent::dump_config() { + LOG_SENSOR(" ", "SDP3X", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, " Connection with SDP3X failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +void SDP3XComponent::read_pressure_() { + uint8_t data[9]; + if (!this->read_bytes_raw(data, 9)) { + ESP_LOGW(TAG, "Couldn't read SDP3X data!"); + this->status_set_warning(); + return; + } + + if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) { + ESP_LOGW(TAG, "Invalid SDP3X data!"); + this->status_set_warning(); + return; + } + + int16_t pressure_raw = encode_uint16(data[0], data[1]); + float pressure = pressure_raw / pressure_scale_factor_; + ESP_LOGV(TAG, "Got raw pressure=%d, scale factor =%.3f ", pressure_raw, pressure_scale_factor_); + ESP_LOGD(TAG, "Got Pressure=%.3f hPa", pressure); + + this->publish_state(pressure); + this->status_clear_warning(); +} + +float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } + +// Check CRC function from SDP3X sample code provided by sensirion +// Returns true if a checksum is OK +bool SDP3XComponent::check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum) { + uint8_t crc = 0xFF; + + // calculates 8-Bit checksum with given polynomial 0x31 (x^8 + x^5 + x^4 + 1) + for (int i = 0; i < size; i++) { + crc ^= (data[i]); + for (uint8_t bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc = (crc << 1); + } + } + + // verify checksum + return (crc == checksum); +} + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h new file mode 100644 index 0000000000..51c9973c61 --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sdp3x { + +class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + /// Schedule temperature+pressure readings. + void update() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + /// Internal method to read the pressure from the component after it has been scheduled. + void read_pressure_(); + + bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum); + + float pressure_scale_factor_ = 0.0f; // hPa per count +}; + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py new file mode 100644 index 0000000000..08d7250f6e --- /dev/null +++ b/esphome/components/sdp3x/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_HECTOPASCAL, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] + +sdp3x_ns = cg.esphome_ns.namespace("sdp3x") +SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=3, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(SDP3XComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x21)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index af482839a9..0997b47ef6 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_PM_2_5, CONF_RX_ONLY, CONF_UPDATE_INTERVAL, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -39,18 +38,16 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SDS011Component), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_RX_ONLY, default=False): cv.boolean, cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_minutes, diff --git a/esphome/components/selec_meter/__init__.py b/esphome/components/selec_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp new file mode 100644 index 0000000000..8bcf91f3c0 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -0,0 +1,108 @@ +#include "selec_meter.h" +#include "selec_meter_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace selec_meter { + +static const char *const TAG = "selec_meter"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers + +void SelecMeter::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for SelecMeter!"); + return; + } + + auto selec_meter_get_float = [&](size_t i, float unit) -> float { + uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]); + + float f; + memcpy(&f, &temp, sizeof(f)); + return (f * unit); + }; + + float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT); + float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT); + float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT); + float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT); + float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT); + float maximum_demand_active_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_reactive_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_apparent_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + + if (this->total_active_energy_sensor_ != nullptr) + this->total_active_energy_sensor_->publish_state(total_active_energy); + if (this->import_active_energy_sensor_ != nullptr) + this->import_active_energy_sensor_->publish_state(import_active_energy); + if (this->export_active_energy_sensor_ != nullptr) + this->export_active_energy_sensor_->publish_state(export_active_energy); + if (this->total_reactive_energy_sensor_ != nullptr) + this->total_reactive_energy_sensor_->publish_state(total_reactive_energy); + if (this->import_reactive_energy_sensor_ != nullptr) + this->import_reactive_energy_sensor_->publish_state(import_reactive_energy); + if (this->export_reactive_energy_sensor_ != nullptr) + this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); + if (this->apparent_energy_sensor_ != nullptr) + this->apparent_energy_sensor_->publish_state(apparent_energy); + if (this->active_power_sensor_ != nullptr) + this->active_power_sensor_->publish_state(active_power); + if (this->reactive_power_sensor_ != nullptr) + this->reactive_power_sensor_->publish_state(reactive_power); + if (this->apparent_power_sensor_ != nullptr) + this->apparent_power_sensor_->publish_state(apparent_power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->maximum_demand_active_power_sensor_ != nullptr) + this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power); + if (this->maximum_demand_reactive_power_sensor_ != nullptr) + this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power); + if (this->maximum_demand_apparent_power_sensor_ != nullptr) + this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power); +} + +void SelecMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SelecMeter::dump_config() { + ESP_LOGCONFIG(TAG, "SELEC Meter:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_sensor_); + LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); + LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); + LOG_SENSOR(" ", "Total Reactive Energy", this->total_reactive_energy_sensor_); + LOG_SENSOR(" ", "Import Reactive Energy", this->import_reactive_energy_sensor_); + LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); + LOG_SENSOR(" ", "Apparent Energy", this->apparent_energy_sensor_); + LOG_SENSOR(" ", "Active Power", this->active_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Maximum Demand Active Power", this->maximum_demand_active_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Reactive Power", this->maximum_demand_reactive_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Apparent Power", this->maximum_demand_apparent_power_sensor_); +} + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h new file mode 100644 index 0000000000..0477cd2a62 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace selec_meter { + +#define SELEC_METER_SENSOR(name) \ + protected: \ + sensor::Sensor *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *(name)) { this->name##_sensor_ = name; } + +class SelecMeter : public PollingComponent, public modbus::ModbusDevice { + public: + SELEC_METER_SENSOR(total_active_energy) + SELEC_METER_SENSOR(import_active_energy) + SELEC_METER_SENSOR(export_active_energy) + SELEC_METER_SENSOR(total_reactive_energy) + SELEC_METER_SENSOR(import_reactive_energy) + SELEC_METER_SENSOR(export_reactive_energy) + SELEC_METER_SENSOR(apparent_energy) + SELEC_METER_SENSOR(active_power) + SELEC_METER_SENSOR(reactive_power) + SELEC_METER_SENSOR(apparent_power) + SELEC_METER_SENSOR(voltage) + SELEC_METER_SENSOR(current) + SELEC_METER_SENSOR(power_factor) + SELEC_METER_SENSOR(frequency) + SELEC_METER_SENSOR(maximum_demand_active_power) + SELEC_METER_SENSOR(maximum_demand_reactive_power) + SELEC_METER_SENSOR(maximum_demand_apparent_power) + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; +}; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter_registers.h b/esphome/components/selec_meter/selec_meter_registers.h new file mode 100644 index 0000000000..dfaf65ff08 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter_registers.h @@ -0,0 +1,32 @@ +#pragma once + +namespace esphome { +namespace selec_meter { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; +static const float NO_DEC_UNIT = 1; +static const float MULTIPLY_TEN_UNIT = 10; +static const float MULTIPLY_THOUSAND_UNIT = 1000; + +/* PHASE STATUS REGISTERS */ +static const uint16_t SELEC_TOTAL_ACTIVE_ENERGY = 0x0000; +static const uint16_t SELEC_IMPORT_ACTIVE_ENERGY = 0x0002; +static const uint16_t SELEC_EXPORT_ACTIVE_ENERGY = 0x0004; +static const uint16_t SELEC_TOTAL_REACTIVE_ENERGY = 0x0006; +static const uint16_t SELEC_IMPORT_REACTIVE_ENERGY = 0x0008; +static const uint16_t SELEC_EXPORT_REACTIVE_ENERGY = 0x000A; +static const uint16_t SELEC_APPARENT_ENERGY = 0x000C; +static const uint16_t SELEC_ACTIVE_POWER = 0x000E; +static const uint16_t SELEC_REACTIVE_POWER = 0x0010; +static const uint16_t SELEC_APPARENT_POWER = 0x0012; +static const uint16_t SELEC_VOLTAGE = 0x0014; +static const uint16_t SELEC_CURRENT = 0x0016; +static const uint16_t SELEC_POWER_FACTOR = 0x0018; +static const uint16_t SELEC_FREQUENCY = 0x001A; +static const uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C; +static const uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E; +static const uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py new file mode 100644 index 0000000000..2d05d00380 --- /dev/null +++ b/esphome/components/selec_meter/sensor.py @@ -0,0 +1,180 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_CURRENT, + CONF_EXPORT_ACTIVE_ENERGY, + CONF_EXPORT_REACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_IMPORT_ACTIVE_ENERGY, + CONF_IMPORT_REACTIVE_ENERGY, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + LAST_RESET_TYPE_AUTO, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, +) + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@sourabhjaiswal"] + +CONF_TOTAL_ACTIVE_ENERGY = "total_active_energy" +CONF_TOTAL_REACTIVE_ENERGY = "total_reactive_energy" +CONF_APPARENT_ENERGY = "apparent_energy" +CONF_MAXIMUM_DEMAND_ACTIVE_POWER = "maximum_demand_active_power" +CONF_MAXIMUM_DEMAND_REACTIVE_POWER = "maximum_demand_reactive_power" +CONF_MAXIMUM_DEMAND_APPARENT_POWER = "maximum_demand_apparent_power" + +UNIT_KILOWATT_HOURS = "kWh" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" + +selec_meter_ns = cg.esphome_ns.namespace("selec_meter") +SelecMeter = selec_meter_ns.class_( + "SelecMeter", cg.PollingComponent, modbus.ModbusDevice +) + +SENSORS = { + CONF_TOTAL_ACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_APPARENT_ENERGY: sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_REACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_POWER_FACTOR: sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_FREQUENCY: sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), +} + +CONFIG_SCHEMA = ( + cv.Schema({cv.GenerateID(): cv.declare_id(SelecMeter)}) + .extend( + {cv.Optional(sensor_name): schema for sensor_name, schema in SENSORS.items()} + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + for name in SENSORS: + if name in config: + sens = await sensor.new_sensor(config[name]) + cg.add(getattr(var, f"set_{name}_sensor")(sens)) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py new file mode 100644 index 0000000000..d3ab344926 --- /dev/null +++ b/esphome/components/select/__init__.py @@ -0,0 +1,104 @@ +from typing import List +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, + CONF_ICON, + CONF_ID, + CONF_INTERNAL, + CONF_ON_VALUE, + CONF_OPTION, + CONF_TRIGGER_ID, + CONF_NAME, + CONF_MQTT_ID, + ICON_EMPTY, +) +from esphome.core import CORE, coroutine_with_priority + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +select_ns = cg.esphome_ns.namespace("select") +Select = select_ns.class_("Select", cg.Nameable) +SelectPtr = Select.operator("ptr") + +# Triggers +SelectStateTrigger = select_ns.class_( + "SelectStateTrigger", automation.Trigger.template(cg.float_) +) + +# Actions +SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) + +icon = cv.icon + + +SELECT_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), + cv.GenerateID(): cv.declare_id(Select), + cv.Optional(CONF_ICON, default=ICON_EMPTY): icon, + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger), + } + ), + } +) + + +async def setup_select_core_(var, config, *, options: List[str]): + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + if CONF_INTERNAL in config: + cg.add(var.set_internal(config[CONF_INTERNAL])) + + cg.add(var.traits.set_icon(config[CONF_ICON])) + cg.add(var.traits.set_options(options)) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_select(var, config, *, options: List[str]): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_select(var)) + await setup_select_core_(var, config, options=options) + + +async def new_select(config, *, options: List[str]): + var = cg.new_Pvariable(config[CONF_ID]) + await register_select(var, config, options=options) + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_SELECT") + cg.add_global(select_ns.using) + + +@automation.register_action( + "select.set", + SelectSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Select), + cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), + } + ), +) +async def select_set_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_OPTION], args, str) + cg.add(var.set_option(template_)) + return var diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h new file mode 100644 index 0000000000..59525f879e --- /dev/null +++ b/esphome/components/select/automation.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "select.h" + +namespace esphome { +namespace select { + +class SelectStateTrigger : public Trigger { + public: + explicit SelectStateTrigger(Select *parent) { + parent->add_on_state_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +template class SelectSetAction : public Action { + public: + SelectSetAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(std::string, option) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.set_option(this->option_.value(x...)); + call.perform(); + } + + protected: + Select *select_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp new file mode 100644 index 0000000000..14f4d9277d --- /dev/null +++ b/esphome/components/select/select.cpp @@ -0,0 +1,43 @@ +#include "select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace 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) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); + this->state_callback_.call(state); +} + +void Select::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +uint32_t Select::hash_base() { return 2812997003UL; } + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h new file mode 100644 index 0000000000..414a8daabb --- /dev/null +++ b/esphome/components/select/select.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace select { + +#define LOG_SELECT(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + if (!(obj)->traits.get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->traits.get_icon().c_str()); \ + } \ + } + +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 &get_option() const { return option_; } + + protected: + Select *const parent_; + optional option_; +}; + +class SelectTraits { + public: + void set_options(std::vector options) { this->options_ = std::move(options); } + const std::vector get_options() const { return this->options_; } + void set_icon(std::string icon) { icon_ = std::move(icon); } + const std::string &get_icon() const { return icon_; } + + protected: + std::vector options_; + std::string icon_; +}; + +/** Base-class for all selects. + * + * A select can use publish_state to send out a new value. + */ +class Select : public Nameable { + public: + std::string state; + + void publish_state(const std::string &state); + + SelectCall make_call() { return SelectCall(this); } + void set(const std::string &value) { make_call().set_option(value).perform(); } + + void add_on_state_callback(std::function &&callback); + + SelectTraits traits; + + /// Return whether this select has gotten a full state yet. + bool has_state() const { return has_state_; } + + protected: + friend class SelectCall; + + /** Set the value of the select, this is a virtual method that each select integration must implement. + * + * This method is called by the SelectCall. + * + * @param value The value as validated by the SelectCall. + */ + virtual void control(const std::string &value) = 0; + + uint32_t hash_base() override; + + CallbackManager state_callback_; + bool has_state_{false}; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 2d40e12a09..739a8ada50 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -6,7 +6,6 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_CO2, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, @@ -39,11 +38,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SenseAirComponent), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 0a0c3a9214..ebd4fcc26c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,5 +1,4 @@ import math -from typing import Optional import esphome.codegen as cg import esphome.config_validation as cv @@ -11,6 +10,7 @@ from esphome.const import ( CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, + CONF_DISABLED_BY_DEFAULT, CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, @@ -34,8 +34,6 @@ from esphome.const import ( LAST_RESET_TYPE_AUTO, LAST_RESET_TYPE_NEVER, LAST_RESET_TYPE_NONE, - UNIT_EMPTY, - ICON_EMPTY, DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_MONOXIDE, @@ -44,6 +42,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_POWER, @@ -51,7 +50,6 @@ from esphome.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, - STATE_CLASS_NONE, ) from esphome.core import CORE, coroutine_with_priority from esphome.util import Registry @@ -66,6 +64,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, @@ -167,19 +166,19 @@ CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter) CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter) SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) -unit_of_measurement = cv.string_strict -accuracy_decimals = cv.int_ -icon = cv.icon -device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +validate_unit_of_measurement = cv.string_strict +validate_accuracy_decimals = cv.int_ +validate_icon = cv.icon +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( +SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), cv.GenerateID(): cv.declare_id(Sensor), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): unit_of_measurement, - cv.Optional(CONF_ICON): icon, - cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, - cv.Optional(CONF_DEVICE_CLASS): device_class, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, + cv.Optional(CONF_ICON): validate_icon, + cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_STATE_CLASS): validate_state_class, cv.Optional(CONF_LAST_RESET_TYPE): validate_last_reset_type, cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, @@ -209,41 +208,55 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( } ) +_UNDEF = object() + def sensor_schema( - unit_of_measurement_: str, - icon_: str, - accuracy_decimals_: int, - device_class_: Optional[str] = DEVICE_CLASS_EMPTY, - state_class_: Optional[str] = STATE_CLASS_NONE, - last_reset_type_: Optional[str] = LAST_RESET_TYPE_NONE, + unit_of_measurement: str = _UNDEF, + icon: str = _UNDEF, + accuracy_decimals: int = _UNDEF, + device_class: str = _UNDEF, + state_class: str = _UNDEF, + last_reset_type: str = _UNDEF, ) -> cv.Schema: schema = SENSOR_SCHEMA - if unit_of_measurement_ != UNIT_EMPTY: + if unit_of_measurement is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement_ - ): unit_of_measurement + CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement + ): validate_unit_of_measurement } ) - if icon_ != ICON_EMPTY: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon_): icon}) - if accuracy_decimals_ != 0: + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): validate_icon}) + if accuracy_decimals is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_ACCURACY_DECIMALS, default=accuracy_decimals_ - ): accuracy_decimals, + CONF_ACCURACY_DECIMALS, default=accuracy_decimals + ): validate_accuracy_decimals, } ) - if device_class_ != DEVICE_CLASS_EMPTY: + if device_class is not _UNDEF: schema = schema.extend( - {cv.Optional(CONF_DEVICE_CLASS, default=device_class_): device_class} + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } ) - if state_class_ != STATE_CLASS_NONE: + if state_class is not _UNDEF: schema = schema.extend( - {cv.Optional(CONF_STATE_CLASS, default=state_class_): validate_state_class} + {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} + ) + if last_reset_type is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_LAST_RESET_TYPE, default=last_reset_type + ): validate_last_reset_type + } ) if last_reset_type_ != LAST_RESET_TYPE_NONE: schema = schema.extend( @@ -490,6 +503,7 @@ async def build_filters(config): async def setup_sensor_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_DEVICE_CLASS in config: diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 7a3e870f6d..3e33af3b4a 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -4,14 +4,12 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, CONF_BASELINE, - DEVICE_CLASS_EMPTY, CONF_ECO2, CONF_TVOC, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, - UNIT_EMPTY, ICON_MOLECULE_CO2, ) @@ -33,24 +31,24 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SGP30Component), cv.Required(CONF_ECO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( - UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( - UNIT_EMPTY, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, ), cv.Optional(CONF_TVOC_BASELINE): sensor.sensor_schema( - UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY + icon=ICON_RADIATOR, + accuracy_decimals=0, ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_BASELINE): cv.Schema( diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index fdc6ae031d..e30d6d3adc 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -47,7 +47,7 @@ void SGP30Component::setup() { } this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // Featureset identification for future use if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) { @@ -245,7 +245,7 @@ void SGP30Component::dump_config() { break; } } else { - ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { ESP_LOGCONFIG(TAG, " Baseline:"); ESP_LOGCONFIG(TAG, " eCO2 Baseline: 0x%04X", this->eco2_baseline_); diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 36e039d2b5..0f562048ac 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -3,10 +3,8 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) DEPENDENCIES = ["i2c"] @@ -26,7 +24,9 @@ CONF_VOC_BASELINE = "voc_baseline" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 3b634353c4..8d93b3e1b1 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -23,7 +23,7 @@ void SGP40Component::setup() { } this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // Featureset identification for future use if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) { @@ -248,7 +248,7 @@ void SGP40Component::dump_config() { break; } } else { - ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT); } LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index 2a4bb6594e..b9e7bce733 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHT3XDComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index a746ecde07..a66ca1a526 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -51,18 +51,18 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHT4XComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_WATER_PERCENT, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + 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_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS), cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum( diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index af9379218c..ba2283a9b4 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHTCXComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py index 3d522b3bd5..73cada0eb3 100644 --- a/esphome/components/sm300d2/sensor.py +++ b/esphome/components/sm300d2/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_PM_10_0, CONF_TEMPERATURE, CONF_HUMIDITY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT, @@ -18,7 +17,6 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, UNIT_PERCENT, - ICON_EMPTY, ICON_MOLECULE_CO2, ICON_FLASK, ICON_CHEMICAL_WEAPON, @@ -35,53 +33,46 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SM300D2Sensor), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_FLASK, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_FLASK, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TVOC): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_GRAIN, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GRAIN, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_GRAIN, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GRAIN, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index b667f3b1ce..ff176b1d4e 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -8,6 +8,11 @@ #include "sntp.h" #endif +// Yes, the server names are leaked, but that's fine. +#ifdef CLANG_TIDY +#define strdup(x) (const_cast(x)) +#endif + namespace esphome { namespace sntp { diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 219f68c5c8..959b427861 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -13,7 +13,6 @@ from esphome.const import ( CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, @@ -33,74 +32,64 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SPS30Component), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_4_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + 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_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_4_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_SIZE): sensor.sensor_schema( - UNIT_MICROMETER, - ICON_RULER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 10e66df784..d321933e8f 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -7,8 +7,6 @@ namespace ssd1306_base { static const char *const TAG = "ssd1306"; -static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; static const uint8_t SSD1306_MAX_CONTRAST = 255; static const uint8_t SSD1306_COMMAND_DISPLAY_OFF = 0xAE; @@ -89,8 +87,8 @@ void SSD1306::setup() { set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory this->turn_on(); } diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index 007f61d5c8..520248a66e 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -106,9 +106,9 @@ void SSD1322::setup() { this->data(180); this->command(SSD1322_ENABLEGRAYSCALETABLE); set_brightness(this->brightness_); - this->fill(COLOR_BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1322::display() { this->command(SSD1322_SETCOLUMNADDRESS); // set column address diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 1cca1853b1..60e46f573f 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -7,8 +7,6 @@ namespace ssd1325_base { static const char *const TAG = "ssd1325"; -static const uint8_t BLACK = 0; -static const uint8_t WHITE = 15; static const uint8_t SSD1325_MAX_CONTRAST = 127; static const uint8_t SSD1325_COLORMASK = 0x0f; static const uint8_t SSD1325_COLORSHIFT = 4; @@ -114,9 +112,9 @@ void SSD1325::setup() { this->command(0x0D | 0x02); this->command(SSD1325_NORMALDISPLAY); // set display mode set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1325::display() { this->command(SSD1325_SETCOLADDR); // set column address diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index ae94be87df..4cb8d17a3d 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -78,9 +78,9 @@ void SSD1327::setup() { this->command(0x1C); this->command(SSD1327_NORMALDISPLAY); // set display mode set_brightness(this->brightness_); - this->fill(COLOR_BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1327::display() { this->command(SSD1327_SETCOLUMNADDRESS); // set column address diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index f25ef50075..88764c3d90 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -7,8 +7,6 @@ namespace ssd1331_base { static const char *const TAG = "ssd1331"; -static const uint16_t BLACK = 0; -static const uint16_t WHITE = 0xffff; static const uint16_t SSD1331_COLORMASK = 0xffff; static const uint8_t SSD1331_MAX_CONTRASTA = 0x91; static const uint8_t SSD1331_MAX_CONTRASTB = 0x50; @@ -19,7 +17,7 @@ static const uint8_t SSD1331_DRAWLINE = 0x21; // Draw line static const uint8_t SSD1331_DRAWRECT = 0x22; // Draw rectangle static const uint8_t SSD1331_FILL = 0x26; // Fill enable/disable static const uint8_t SSD1331_SETCOLUMN = 0x15; // Set column address -static const uint8_t SSD1331_SETROW = 0x75; // Set row adress +static const uint8_t SSD1331_SETROW = 0x75; // Set row address static const uint8_t SSD1331_CONTRASTA = 0x81; // Set contrast for color A static const uint8_t SSD1331_CONTRASTB = 0x82; // Set contrast for color B static const uint8_t SSD1331_CONTRASTC = 0x83; // Set contrast for color C @@ -78,9 +76,9 @@ void SSD1331::setup() { this->command(SSD1331_MASTERCURRENT); // 0x87 this->command(0x06); set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1331::display() { this->command(SSD1331_SETCOLUMN); // set column address diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp index 34f357e38a..f26cd7c697 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.cpp +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -7,8 +7,6 @@ namespace ssd1351_base { static const char *const TAG = "ssd1351"; -static const uint16_t BLACK = 0; -static const uint16_t WHITE = 0xffff; static const uint16_t SSD1351_COLORMASK = 0xffff; static const uint8_t SSD1351_MAX_CONTRAST = 15; static const uint8_t SSD1351_BYTESPERPIXEL = 2; @@ -87,9 +85,9 @@ void SSD1351::setup() { this->data(0x80); this->data(0xC8); set_brightness(this->brightness_); - this->fill(BLACK); // clear display - ensures we do not see garbage at power-on - this->display(); // ...write buffer, which actually clears the display's memory - this->turn_on(); // display ON + this->fill(Color::BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1351::display() { this->command(SSD1351_SETCOLUMN); // set column address diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index f329ef4620..0467ed83db 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -220,8 +220,7 @@ static const uint8_t PROGMEM // clang-format on static const char *const TAG = "st7735"; -ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, - boolean usebgr) { +ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr) { model_ = model; this->width_ = width; this->height_ = height; diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 11bcc746f0..737170e99b 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -37,7 +37,7 @@ class ST7735 : public PollingComponent, public spi::SPIDevice { public: - ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, boolean usebgr); + ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr); void dump_config() override; void setup() override; @@ -75,8 +75,8 @@ class ST7735 : public PollingComponent, ST7735Model model_{ST7735_INITR_18BLACKTAB}; uint8_t colstart_ = 0, rowstart_ = 0; - boolean eightbitcolor_ = false; - boolean usebgr_ = false; + bool eightbitcolor_ = false; + bool usebgr_ = false; int16_t width_ = 80, height_ = 80; // Watch heap size GPIOPin *reset_pin_{nullptr}; diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 0e17e65fd7..2aef043ba0 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -19,13 +19,13 @@ static const uint8_t ST7789_RDDMADCTL = 0x0B; // Read Display MADCTL static const uint8_t ST7789_RDDCOLMOD = 0x0C; // Read Display Pixel Format static const uint8_t ST7789_RDDIM = 0x0D; // Read Display Image Mode static const uint8_t ST7789_RDDSM = 0x0E; // Read Display Signal Mod -static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Resul +static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Result static const uint8_t ST7789_SLPIN = 0x10; // Sleep in static const uint8_t ST7789_SLPOUT = 0x11; // Sleep Out -static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode O -static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode O +static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode On +static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode On static const uint8_t ST7789_INVOFF = 0x20; // Display Inversion Off -static const uint8_t ST7789_INVON = 0x21; // Display Inversion O +static const uint8_t ST7789_INVON = 0x21; // Display Inversion On static const uint8_t ST7789_GAMSET = 0x26; // Gamma Set static const uint8_t ST7789_DISPOFF = 0x28; // Display Off static const uint8_t ST7789_DISPON = 0x29; // Display On @@ -34,18 +34,18 @@ static const uint8_t ST7789_RASET = 0x2B; // Row Address Set static const uint8_t ST7789_RAMWR = 0x2C; // Memory Write static const uint8_t ST7789_RAMRD = 0x2E; // Memory Read static const uint8_t ST7789_PTLAR = 0x30; // Partial Area -static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definitio -static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line OFF +static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definition +static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line Off static const uint8_t ST7789_TEON = 0x35; // Tearing Effect Line On static const uint8_t ST7789_MADCTL = 0x36; // Memory Data Access Control static const uint8_t ST7789_VSCSAD = 0x37; // Vertical Scroll Start Address of RAM static const uint8_t ST7789_IDMOFF = 0x38; // Idle Mode Off -static const uint8_t ST7789_IDMON = 0x39; // Idle mode on +static const uint8_t ST7789_IDMON = 0x39; // Idle Mode On static const uint8_t ST7789_COLMOD = 0x3A; // Interface Pixel Format static const uint8_t ST7789_WRMEMC = 0x3C; // Write Memory Continue static const uint8_t ST7789_RDMEMC = 0x3E; // Read Memory Continue static const uint8_t ST7789_STE = 0x44; // Set Tear Scanline -static const uint8_t ST7789_GSCAN = 0x45; // Get Scanlin +static const uint8_t ST7789_GSCAN = 0x45; // Get Scanline static const uint8_t ST7789_WRDISBV = 0x51; // Write Display Brightness static const uint8_t ST7789_RDDISBV = 0x52; // Read Display Brightness Value static const uint8_t ST7789_WRCTRLD = 0x53; // Write CTRL Display @@ -59,17 +59,17 @@ static const uint8_t ST7789_RDID1 = 0xDA; // Read ID1 static const uint8_t ST7789_RDID2 = 0xDB; // Read ID2 static const uint8_t ST7789_RDID3 = 0xDC; // Read ID3 static const uint8_t ST7789_RAMCTRL = 0xB0; // RAM Control -static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Contro +static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Control static const uint8_t ST7789_PORCTRL = 0xB2; // Porch Setting static const uint8_t ST7789_FRCTRL1 = 0xB3; // Frame Rate Control 1 (In partial mode/ idle colors) -static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Contro -static const uint8_t ST7789_GCTRL = 0xB7; // Gate Contro -static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustmen +static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Control +static const uint8_t ST7789_GCTRL = 0xB7; // Gate Control +static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustment static const uint8_t ST7789_DGMEN = 0xBA; // Digital Gamma Enable static const uint8_t ST7789_VCOMS = 0xBB; // VCOMS Setting static const uint8_t ST7789_LCMCTRL = 0xC0; // LCM Control -static const uint8_t ST7789_IDSET = 0xC1; // ID Code Settin -static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enabl +static const uint8_t ST7789_IDSET = 0xC1; // ID Code Setting +static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enable static const uint8_t ST7789_VRHS = 0xC3; // VRH Set static const uint8_t ST7789_VDVS = 0xC4; // VDV Set static const uint8_t ST7789_VCMOFSET = 0xC5; // VCOMS Offset Set @@ -89,8 +89,8 @@ static const uint8_t ST7789_GATECTRL = 0xE4; // Gate Control static const uint8_t ST7789_SPI2EN = 0xE7; // SPI2 Enable static const uint8_t ST7789_PWCTRL2 = 0xE8; // Power Control 2 static const uint8_t ST7789_EQCTRL = 0xE9; // Equalize time control -static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Contro -static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enabl +static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Control +static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enable static const uint8_t ST7789_NVMSET = 0xFC; // NVM Setting static const uint8_t ST7789_PROMACT = 0xFE; // Program action diff --git a/esphome/components/status_led/light/__init__.py b/esphome/components/status_led/light/__init__.py new file mode 100644 index 0000000000..8896046998 --- /dev/null +++ b/esphome/components/status_led/light/__init__.py @@ -0,0 +1,26 @@ +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light +from esphome.const import CONF_OUTPUT_ID, CONF_PIN +from .. import status_led_ns + +StatusLEDLightOutput = status_led_ns.class_( + "StatusLEDLightOutput", light.LightOutput, cg.Component +) + +CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(StatusLEDLightOutput), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + await cg.register_component(var, config) + # cg.add(cg.App.register_component(var)) + await light.register_light(var, config) diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp new file mode 100644 index 0000000000..760c89f972 --- /dev/null +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -0,0 +1,68 @@ +#include "status_led_light.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace status_led { + +static const char *const TAG = "status_led"; + +void StatusLEDLightOutput::loop() { + uint32_t new_state = App.get_app_state() & STATUS_LED_MASK; + + if (new_state != this->last_app_state_) { + ESP_LOGV(TAG, "New app state 0x%08X", new_state); + } + + if ((new_state & STATUS_LED_ERROR) != 0u) { + this->pin_->digital_write(millis() % 250u < 150u); + this->last_app_state_ = new_state; + } else if ((new_state & STATUS_LED_WARNING) != 0u) { + this->pin_->digital_write(millis() % 1500u < 250u); + this->last_app_state_ = new_state; + } else if (new_state != this->last_app_state_) { + // if no error/warning -> restore light state or turn off + bool state = false; + + if (lightstate_) + lightstate_->current_values_as_binary(&state); + + this->pin_->digital_write(state); + this->last_app_state_ = new_state; + + ESP_LOGD(TAG, "Restoring light state %s", ONOFF(state)); + } +} + +void StatusLEDLightOutput::setup_state(light::LightState *state) { + lightstate_ = state; + ESP_LOGD(TAG, "'%s': Setting initital state", state->get_name().c_str()); + this->write_state(state); +} + +void StatusLEDLightOutput::write_state(light::LightState *state) { + bool binary; + state->current_values_as_binary(&binary); + + // if in warning/error, don't overwrite the status_led + // once it is back to OK, the loop will restore the state + if ((App.get_app_state() & (STATUS_LED_ERROR | STATUS_LED_WARNING)) == 0u) { + this->pin_->digital_write(binary); + ESP_LOGD(TAG, "'%s': Setting state %s", state->get_name().c_str(), ONOFF(binary)); + } +} + +void StatusLEDLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up Status LED..."); + + this->pin_->setup(); + this->pin_->digital_write(false); +} + +void StatusLEDLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Status Led Light:"); + LOG_PIN(" Pin: ", this->pin_); +} + +} // namespace status_led +} // namespace esphome diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h new file mode 100644 index 0000000000..8a7f4b4da8 --- /dev/null +++ b/esphome/components/status_led/light/status_led_light.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace status_led { + +class StatusLEDLightOutput : public light::LightOutput, public Component { + public: + void set_pin(GPIOPin *pin) { pin_ = pin; } + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); + return traits; + } + + void loop() override; + + void setup_state(light::LightState *state) override; + + void write_state(light::LightState *state) override; + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_loop_priority() const override { return 50.0f; } + + protected: + GPIOPin *pin_; + light::LightState *lightstate_{}; + uint32_t last_app_state_{0xFFFF}; +}; + +} // namespace status_led +} // namespace esphome diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py index 9de077c20a..b02c835ef8 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -19,7 +18,10 @@ STS3XComponent = sts3x_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index 644490ffc6..236acfadef 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_DEGREES, ICON_WEATHER_SUNSET, @@ -22,7 +21,10 @@ TYPES = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_DEGREES, ICON_WEATHER_SUNSET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_WEATHER_SUNSET, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 647041c19c..8aa213a9f6 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.automation import Condition, maybe_simple_id from esphome.components import mqtt from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_ID, CONF_INTERNAL, @@ -38,7 +39,7 @@ SwitchTurnOffTrigger = switch_ns.class_( icon = cv.icon -SWITCH_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( +SWITCH_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), cv.Optional(CONF_ICON): icon, @@ -59,6 +60,7 @@ SWITCH_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend( async def setup_switch_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h index d73f397f16..b97b85993f 100644 --- a/esphome/components/sx1509/sx1509_registers.h +++ b/esphome/components/sx1509/sx1509_registers.h @@ -9,7 +9,7 @@ Here you'll find the Arduino code used to interface with the SX1509 I2C 16 I/O expander. There are functions to take advantage of everything the SX1509 provides - input/output setting, writing pins high/low, reading the input value of pins, LED driver utilities (blink, breath, pwm), and -keypad engine utilites. +keypad engine utilities. Development environment specifics: IDE: Arduino 1.6.5 diff --git a/esphome/components/t6615/__init__.py b/esphome/components/t6615/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py new file mode 100644 index 0000000000..71a099d635 --- /dev/null +++ b/esphome/components/t6615/sensor.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@tylermenezes"] +DEPENDENCIES = ["uart"] + +t6615_ns = cg.esphome_ns.namespace("t6615") +T6615Component = t6615_ns.class_("T6615Component", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(T6615Component), + cv.Required(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "t6615", baud_rate=19200, require_rx=True, require_tx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp new file mode 100644 index 0000000000..09ff61827c --- /dev/null +++ b/esphome/components/t6615/t6615.cpp @@ -0,0 +1,81 @@ +#include "t6615.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace t6615 { + +static const char *const TAG = "t6615"; + +static const uint8_t T6615_RESPONSE_BUFFER_LENGTH = 32; +static const uint8_t T6615_MAGIC = 0xFF; +static const uint8_t T6615_ADDR_HOST = 0xFA; +static const uint8_t T6615_ADDR_SENSOR = 0xFE; +static const uint8_t T6615_COMMAND_GET_PPM[] = {0x02, 0x03}; +static const uint8_t T6615_COMMAND_GET_SERIAL[] = {0x02, 0x01}; +static const uint8_t T6615_COMMAND_GET_VERSION[] = {0x02, 0x0D}; +static const uint8_t T6615_COMMAND_GET_ELEVATION[] = {0x02, 0x0F}; +static const uint8_t T6615_COMMAND_GET_ABC[] = {0xB7, 0x00}; +static const uint8_t T6615_COMMAND_ENABLE_ABC[] = {0xB7, 0x01}; +static const uint8_t T6615_COMMAND_DISABLE_ABC[] = {0xB7, 0x02}; +static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F}; + +void T6615Component::loop() { + if (!this->available()) + return; + + // Read header + uint8_t header[3]; + this->read_array(header, 3); + if (header[0] != T6615_MAGIC || header[1] != T6615_ADDR_HOST) { + ESP_LOGW(TAG, "Reading data from T6615 failed!"); + while (this->available()) + this->read(); // Clear the incoming buffer + this->status_set_warning(); + return; + } + + // Read body + uint8_t length = header[2]; + uint8_t response[T6615_RESPONSE_BUFFER_LENGTH]; + this->read_array(response, length); + + this->status_clear_warning(); + + switch (this->command_) { + case T6615Command::GET_PPM: { + const uint16_t ppm = encode_uint16(response[0], response[1]); + ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); + this->co2_sensor_->publish_state(ppm); + break; + } + default: + break; + } + + this->command_ = T6615Command::NONE; +} + +void T6615Component::update() { this->query_ppm_(); } + +void T6615Component::query_ppm_() { + if (this->co2_sensor_ == nullptr || this->command_ != T6615Command::NONE) { + return; + } + + this->command_ = T6615Command::GET_PPM; + + this->write_byte(T6615_MAGIC); + this->write_byte(T6615_ADDR_SENSOR); + this->write_byte(sizeof(T6615_COMMAND_GET_PPM)); + this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM)); +} + +float T6615Component::get_setup_priority() const { return setup_priority::DATA; } +void T6615Component::dump_config() { + ESP_LOGCONFIG(TAG, "T6615:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(19200); +} + +} // namespace t6615 +} // namespace esphome diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h new file mode 100644 index 0000000000..a7da3b4cf6 --- /dev/null +++ b/esphome/components/t6615/t6615.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace t6615 { + +enum class T6615Command : uint8_t { + NONE = 0, + GET_PPM, + GET_SERIAL, + GET_VERSION, + GET_ELEVATION, + GET_ABC, + ENABLE_ABC, + DISABLE_ABC, + SET_ELEVATION, +}; + +class T6615Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void loop() override; + void update() override; + void dump_config() override; + + void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; } + + protected: + void query_ppm_(); + + T6615Command command_ = T6615Command::NONE; + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace t6615 +} // namespace esphome diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index d0fa0c1732..6c74c86faf 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -7,9 +7,7 @@ from esphome.const import ( CONF_ID, CONF_ILLUMINANCE, CONF_INTEGRATION_TIME, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, @@ -49,13 +47,22 @@ TCS34725_GAINS = { } color_channel_schema = sensor.sensor_schema( - UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) color_temperature_schema = sensor.sensor_schema( - UNIT_KELVIN, ICON_THERMOMETER, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KELVIN, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) illuminance_schema = sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index d7bf8999ef..9a5712e10f 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) - yield cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index ffdb1509be..e7cc2fcb1b 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -9,7 +9,9 @@ CONF_TAG_NAME = "tag_name" TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend( +CONFIG_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0 +).extend( { cv.GenerateID(): cv.declare_id(TeleInfoSensor), cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), @@ -18,9 +20,9 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID]) cg.add(teleinfo.register_teleinfo_listener(var)) diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py index b1ade4df41..3bd73ff272 100644 --- a/esphome/components/teleinfo/text_sensor/__init__.py +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -20,9 +20,9 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) - teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID]) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID]) cg.add(teleinfo.register_teleinfo_listener(var)) diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py new file mode 100644 index 0000000000..4044a407f3 --- /dev/null +++ b/esphome/components/template/select/__init__.py @@ -0,0 +1,74 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import ( + CONF_ID, + CONF_INITIAL_OPTION, + CONF_LAMBDA, + CONF_OPTIONS, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, +) +from .. import template_ns + +TemplateSelect = template_ns.class_( + "TemplateSelect", select.Select, cg.PollingComponent +) + +CONF_SET_ACTION = "set_action" + + +def validate_initial_value_in_options(config): + if CONF_INITIAL_OPTION in config: + if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: + raise cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + ) + else: + config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] + return config + + +CONFIG_SCHEMA = cv.All( + select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSelect), + cv.Required(CONF_OPTIONS): cv.All( + cv.ensure_list(cv.string_strict), cv.Length(min=1) + ), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_OPTION): cv.string_strict, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate_initial_value_in_options, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await select.register_select(var, config, options=config[CONF_OPTIONS]) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(str) + ) + cg.add(var.set_template(template_)) + + else: + if CONF_OPTIMISTIC in config: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION] + ) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp new file mode 100644 index 0000000000..782c0ee6f9 --- /dev/null +++ b/esphome/components/template/select/template_select.cpp @@ -0,0 +1,74 @@ +#include "template_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.select"; + +void TemplateSelect::setup() { + if (this->f_.has_value()) + return; + + std::string value; + ESP_LOGD(TAG, "Setting up Template Number"); + if (!this->restore_value_) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial: %s", value.c_str()); + } else { + size_t index; + this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&index)) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str()); + } else { + value = this->traits.get_options().at(index); + ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + } + } + + this->publish_state(value); +} + +void TemplateSelect::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + auto options = this->traits.get_options(); + if (std::find(options.begin(), options.end(), *val) == options.end()) { + ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str()); + return; + } + + this->publish_state(*val); +} + +void TemplateSelect::control(const std::string &value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->restore_value_) { + auto options = this->traits.get_options(); + size_t index = std::find(options.begin(), options.end(), value) - options.begin(); + + this->pref_.save(&index); + } +} +void TemplateSelect::dump_config() { + LOG_SELECT("", "Template Select", this); + LOG_UPDATE_INTERVAL(this); + if (this->f_.has_value()) + return; + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + ESP_LOGCONFIG(TAG, " Initial Option: %s", this->initial_option_.c_str()); + ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h new file mode 100644 index 0000000000..e24eb6e880 --- /dev/null +++ b/esphome/components/template/select/template_select.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +class TemplateSelect : public select::Select, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_initial_option(std::string initial_option) { this->initial_option_ = std::move(initial_option); } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const std::string &value) override; + bool optimistic_ = false; + std::string initial_option_; + bool restore_value_ = false; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 47027583bf..75fb505d91 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -6,10 +6,7 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_STATE, - DEVICE_CLASS_EMPTY, - ICON_EMPTY, STATE_CLASS_NONE, - UNIT_EMPTY, ) from .. import template_ns @@ -19,11 +16,8 @@ TemplateSensor = template_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 84fedc8d94..d06f12de0e 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import ( + CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_ID, CONF_INTERNAL, @@ -33,7 +34,7 @@ TextSensorStateCondition = text_sensor_ns.class_( icon = cv.icon -TEXT_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( +TEXT_SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), cv.Optional(CONF_ICON): icon, @@ -48,6 +49,7 @@ TEXT_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( async def setup_text_sensor_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 07a94fd184..7b5ee7c624 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -6,7 +6,10 @@ from esphome.const import ( CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, + CONF_COOL_DEADBAND, CONF_COOL_MODE, + CONF_COOL_OVERRUN, + CONF_DEFAULT_MODE, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, @@ -21,22 +24,45 @@ from esphome.const import ( CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, + CONF_FAN_ONLY_COOLING, CONF_FAN_ONLY_MODE, + CONF_FAN_WITH_COOLING, + CONF_FAN_WITH_HEATING, CONF_HEAT_ACTION, + CONF_HEAT_DEADBAND, CONF_HEAT_MODE, - CONF_HYSTERESIS, + CONF_HEAT_OVERRUN, CONF_ID, CONF_IDLE_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_MAX_HEATING_RUN_TIME, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_MIN_FAN_MODE_SWITCHING_TIME, + CONF_MIN_FANNING_OFF_TIME, + CONF_MIN_FANNING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + CONF_MIN_IDLE_TIME, CONF_OFF_MODE, CONF_SENSOR, + CONF_SET_POINT_MINIMUM_DIFFERENTIAL, + CONF_STARTUP_DELAY, + CONF_SUPPLEMENTAL_COOLING_ACTION, + CONF_SUPPLEMENTAL_COOLING_DELTA, + CONF_SUPPLEMENTAL_HEATING_ACTION, + CONF_SUPPLEMENTAL_HEATING_DELTA, CONF_SWING_BOTH_ACTION, CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION, + CONF_TARGET_TEMPERATURE_CHANGE_ACTION, ) CODEOWNERS = ["@kbx81"] +climate_ns = cg.esphome_ns.namespace("climate") thermostat_ns = cg.esphome_ns.namespace("thermostat") ThermostatClimate = thermostat_ns.class_( "ThermostatClimate", climate.Climate, cg.Component @@ -44,104 +70,241 @@ ThermostatClimate = thermostat_ns.class_( ThermostatClimateTargetTempConfig = thermostat_ns.struct( "ThermostatClimateTargetTempConfig" ) +ClimateMode = climate_ns.enum("ClimateMode") +CLIMATE_MODES = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, +} +validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) def validate_thermostat(config): - # verify corresponding climate action action exists for any defined climate mode action - if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE) - ) - if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE) - ) - if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format( - CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE - ) - ) - if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE) - ) - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config - ): - raise cv.Invalid( - "{} must be defined when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, + # verify corresponding action(s) exist(s) for any defined climate mode or action + requirements = { + CONF_AUTO_MODE: [ + CONF_COOL_ACTION, + CONF_HEAT_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_COOL_MODE: [ + CONF_COOL_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_DRY_MODE: [ + CONF_DRY_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_FAN_ONLY_MODE: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_HEAT_MODE: [ + CONF_HEAT_ACTION, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_COOL_ACTION: [ + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_DRY_ACTION: [ + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_HEAT_ACTION: [ + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_SUPPLEMENTAL_COOLING_ACTION: [ + CONF_COOL_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_SUPPLEMENTAL_COOLING_DELTA, + ], + CONF_SUPPLEMENTAL_HEATING_ACTION: [ + CONF_HEAT_ACTION, + CONF_MAX_HEATING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + CONF_SUPPLEMENTAL_HEATING_DELTA, + ], + CONF_MAX_COOLING_RUN_TIME: [ + CONF_COOL_ACTION, + CONF_SUPPLEMENTAL_COOLING_ACTION, + CONF_SUPPLEMENTAL_COOLING_DELTA, + ], + CONF_MAX_HEATING_RUN_TIME: [ + CONF_HEAT_ACTION, + CONF_SUPPLEMENTAL_HEATING_ACTION, + CONF_SUPPLEMENTAL_HEATING_DELTA, + ], + CONF_MIN_COOLING_OFF_TIME: [ + CONF_COOL_ACTION, + ], + CONF_MIN_COOLING_RUN_TIME: [ + CONF_COOL_ACTION, + ], + CONF_MIN_FANNING_OFF_TIME: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_MIN_FANNING_RUN_TIME: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_MIN_HEATING_OFF_TIME: [ + CONF_HEAT_ACTION, + ], + CONF_MIN_HEATING_RUN_TIME: [ + CONF_HEAT_ACTION, + ], + CONF_SUPPLEMENTAL_COOLING_DELTA: [ + CONF_COOL_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_SUPPLEMENTAL_COOLING_ACTION, + ], + CONF_SUPPLEMENTAL_HEATING_DELTA: [ + CONF_HEAT_ACTION, + CONF_MAX_HEATING_RUN_TIME, + CONF_SUPPLEMENTAL_HEATING_ACTION, + ], + } + for config_trigger, req_triggers in requirements.items(): + for req_trigger in req_triggers: + if config_trigger in config and req_trigger not in config: + raise cv.Invalid( + f"{req_trigger} must be defined to use {config_trigger}" + ) + + if CONF_FAN_ONLY_ACTION in config: + # determine validation requirements based on fan_only_action_uses_fan_mode_timer setting + if config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] is True: + requirements = [CONF_MIN_FAN_MODE_SWITCHING_TIME] + else: + requirements = [ + CONF_MIN_FANNING_OFF_TIME, + CONF_MIN_FANNING_RUN_TIME, + ] + for config_req_action in requirements: + if config_req_action not in config: + raise cv.Invalid( + f"{config_req_action} must be defined to use {CONF_FAN_ONLY_ACTION}" + ) + + # for any fan_mode action, confirm min_fan_mode_switching_time is defined + requirements = { + CONF_MIN_FAN_MODE_SWITCHING_TIME: [ + CONF_FAN_MODE_ON_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_AUTO_ACTION, + CONF_FAN_MODE_LOW_ACTION, + CONF_FAN_MODE_MEDIUM_ACTION, + CONF_FAN_MODE_HIGH_ACTION, + CONF_FAN_MODE_MIDDLE_ACTION, + CONF_FAN_MODE_FOCUS_ACTION, + CONF_FAN_MODE_DIFFUSE_ACTION, + ], + } + for req_config_item, config_triggers in requirements.items(): + for config_trigger in config_triggers: + if config_trigger in config and req_config_item not in config: + raise cv.Invalid( + f"{req_config_item} must be defined to use {config_trigger}" + ) + + # determine validation requirements based on fan_only_cooling setting + if config[CONF_FAN_ONLY_COOLING] is True: + requirements = { + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH: [ CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION, - ) - ) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: - raise cv.Invalid( - "{} must be defined when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and ( - CONF_COOL_ACTION not in config and CONF_FAN_ONLY_ACTION not in config - ): - raise cv.Invalid( - "{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION - ) - ) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid( - "{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) + ], + CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], + } + else: + requirements = { + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH: [CONF_COOL_ACTION], + CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], + } + + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in config and req_action in config: + raise cv.Invalid( + f"{config_temp} must be defined when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in config and req_action not in config: + raise cv.Invalid(f"{config_temp} is defined with no {req_action}") if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config - ): + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in away and req_action in config: + raise cv.Invalid( + f"{config_temp} must be defined in away configuration when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in away and req_action not in config: + raise cv.Invalid( + f"{config_temp} is defined in away configuration with no {req_action}" + ) + + # verify default climate mode is valid given above configuration + default_mode = config[CONF_DEFAULT_MODE] + requirements = { + "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], + "COOL": [CONF_COOL_ACTION], + "HEAT": [CONF_HEAT_ACTION], + "DRY": [CONF_DRY_ACTION], + "FAN_ONLY": [CONF_FAN_ONLY_ACTION], + "AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION], + }.get(default_mode, []) + for req in requirements: + if req not in config: raise cv.Invalid( - "{} must be defined in away configuration when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, - CONF_COOL_ACTION, - CONF_FAN_ONLY_ACTION, - ) - ) - if ( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away - and CONF_HEAT_ACTION in config - ): - raise cv.Invalid( - "{} must be defined in away configuration when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and ( - CONF_COOL_ACTION not in config and CONF_FAN_ONLY_ACTION not in config - ): - raise cv.Invalid( - "{} is defined in away configuration with no {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, - CONF_COOL_ACTION, - CONF_FAN_ONLY_ACTION, - ) - ) - if ( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away - and CONF_HEAT_ACTION not in config - ): - raise cv.Invalid( - "{} is defined in away configuration with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) + f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration" ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid( + f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" + ) + if config[CONF_FAN_WITH_HEATING] is True and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid( + f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_HEATING}" + ) + + # if min_fan_mode_switching_time is defined, at least one fan_mode action should be defined + if CONF_MIN_FAN_MODE_SWITCHING_TIME in config: + requirements = [ + CONF_FAN_MODE_ON_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_AUTO_ACTION, + CONF_FAN_MODE_LOW_ACTION, + CONF_FAN_MODE_MEDIUM_ACTION, + CONF_FAN_MODE_HIGH_ACTION, + CONF_FAN_MODE_MIDDLE_ACTION, + CONF_FAN_MODE_FOCUS_ACTION, + CONF_FAN_MODE_DIFFUSE_ACTION, + ] + for config_req_action in requirements: + if config_req_action in config: + return config + raise cv.Invalid( + f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" + ) return config @@ -152,11 +315,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional( + CONF_SUPPLEMENTAL_COOLING_ACTION + ): automation.validate_automation(single=True), cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation( single=True ), cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional( + CONF_SUPPLEMENTAL_HEATING_ACTION + ): automation.validate_automation(single=True), cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), @@ -204,9 +373,42 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation( single=True ), + cv.Optional( + CONF_TARGET_TEMPERATURE_CHANGE_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable( + validate_climate_mode + ), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional( + CONF_SET_POINT_MINIMUM_DIFFERENTIAL, default=0.5 + ): cv.temperature, + cv.Optional(CONF_COOL_DEADBAND, default=0.5): cv.temperature, + cv.Optional(CONF_COOL_OVERRUN, default=0.5): cv.temperature, + cv.Optional(CONF_HEAT_DEADBAND, default=0.5): cv.temperature, + cv.Optional(CONF_HEAT_OVERRUN, default=0.5): cv.temperature, + cv.Optional(CONF_MAX_COOLING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MAX_HEATING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_COOLING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_COOLING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional( + CONF_MIN_FAN_MODE_SWITCHING_TIME + ): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_FANNING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_FANNING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_HEATING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_HEATING_RUN_TIME): cv.positive_time_period_seconds, + cv.Required(CONF_MIN_IDLE_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_SUPPLEMENTAL_COOLING_DELTA): cv.temperature, + cv.Optional(CONF_SUPPLEMENTAL_HEATING_DELTA): cv.temperature, + cv.Optional( + CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, default=False + ): cv.boolean, + cv.Optional(CONF_FAN_ONLY_COOLING, default=False): cv.boolean, + cv.Optional(CONF_FAN_WITH_COOLING, default=False): cv.boolean, + cv.Optional(CONF_FAN_WITH_HEATING, default=False): cv.boolean, + cv.Optional(CONF_STARTUP_DELAY, default=False): cv.boolean, cv.Optional(CONF_AWAY_CONFIG): cv.Schema( { cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, @@ -233,8 +435,18 @@ async def to_code(config): ) sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) + cg.add( + var.set_set_point_minimum_differential( + config[CONF_SET_POINT_MINIMUM_DIFFERENTIAL] + ) + ) cg.add(var.set_sensor(sens)) - cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) + cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) + cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) + cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) if two_points_available is True: cg.add(var.set_supports_two_points(True)) @@ -252,6 +464,72 @@ async def to_code(config): normal_config = ThermostatClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) + + if CONF_MAX_COOLING_RUN_TIME in config: + cg.add( + var.set_cooling_maximum_run_time_in_sec(config[CONF_MAX_COOLING_RUN_TIME]) + ) + + if CONF_MAX_HEATING_RUN_TIME in config: + cg.add( + var.set_heating_maximum_run_time_in_sec(config[CONF_MAX_HEATING_RUN_TIME]) + ) + + if CONF_MIN_COOLING_OFF_TIME in config: + cg.add( + var.set_cooling_minimum_off_time_in_sec(config[CONF_MIN_COOLING_OFF_TIME]) + ) + + if CONF_MIN_COOLING_RUN_TIME in config: + cg.add( + var.set_cooling_minimum_run_time_in_sec(config[CONF_MIN_COOLING_RUN_TIME]) + ) + + if CONF_MIN_FAN_MODE_SWITCHING_TIME in config: + cg.add( + var.set_fan_mode_minimum_switching_time_in_sec( + config[CONF_MIN_FAN_MODE_SWITCHING_TIME] + ) + ) + + if CONF_MIN_FANNING_OFF_TIME in config: + cg.add( + var.set_fanning_minimum_off_time_in_sec(config[CONF_MIN_FANNING_OFF_TIME]) + ) + + if CONF_MIN_FANNING_RUN_TIME in config: + cg.add( + var.set_fanning_minimum_run_time_in_sec(config[CONF_MIN_FANNING_RUN_TIME]) + ) + + if CONF_MIN_HEATING_OFF_TIME in config: + cg.add( + var.set_heating_minimum_off_time_in_sec(config[CONF_MIN_HEATING_OFF_TIME]) + ) + + if CONF_MIN_HEATING_RUN_TIME in config: + cg.add( + var.set_heating_minimum_run_time_in_sec(config[CONF_MIN_HEATING_RUN_TIME]) + ) + + if CONF_SUPPLEMENTAL_COOLING_DELTA in config: + cg.add(var.set_supplemental_cool_delta(config[CONF_SUPPLEMENTAL_COOLING_DELTA])) + + if CONF_SUPPLEMENTAL_HEATING_DELTA in config: + cg.add(var.set_supplemental_heat_delta(config[CONF_SUPPLEMENTAL_HEATING_DELTA])) + + cg.add(var.set_idle_minimum_time_in_sec(config[CONF_MIN_IDLE_TIME])) + + cg.add( + var.set_supports_fan_only_action_uses_fan_mode_timer( + config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] + ) + ) + cg.add(var.set_supports_fan_only_cooling(config[CONF_FAN_ONLY_COOLING])) + cg.add(var.set_supports_fan_with_cooling(config[CONF_FAN_WITH_COOLING])) + cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) + + cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) cg.add(var.set_normal_config(normal_config)) await automation.build_automation( @@ -268,6 +546,12 @@ async def to_code(config): var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION] ) cg.add(var.set_supports_cool(True)) + if CONF_SUPPLEMENTAL_COOLING_ACTION in config: + await automation.build_automation( + var.get_supplemental_cool_action_trigger(), + [], + config[CONF_SUPPLEMENTAL_COOLING_ACTION], + ) if CONF_DRY_ACTION in config: await automation.build_automation( var.get_dry_action_trigger(), [], config[CONF_DRY_ACTION] @@ -283,6 +567,12 @@ async def to_code(config): var.get_heat_action_trigger(), [], config[CONF_HEAT_ACTION] ) cg.add(var.set_supports_heat(True)) + if CONF_SUPPLEMENTAL_HEATING_ACTION in config: + await automation.build_automation( + var.get_supplemental_heat_action_trigger(), + [], + config[CONF_SUPPLEMENTAL_HEATING_ACTION], + ) if CONF_AUTO_MODE in config: await automation.build_automation( var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE] @@ -380,6 +670,12 @@ async def to_code(config): config[CONF_SWING_VERTICAL_ACTION], ) cg.add(var.set_supports_swing_mode_vertical(True)) + if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_temperature_change_trigger(), + [], + config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], + ) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 305db66f16..d9d8b106ea 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -7,10 +7,20 @@ namespace thermostat { static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { + if (this->use_startup_delay_) { + // start timers so that no actions are called for a moment + this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::TIMER_HEATING_OFF); + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + } + // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; // required action may have changed, recompute, refresh - this->switch_to_action_(compute_action_()); + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); // current temperature and possibly action changed, so publish the new state this->publish_state(); }); @@ -21,61 +31,160 @@ void ThermostatClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles temps for us - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + this->mode = this->default_mode_; this->change_away_(false); } // refresh the climate action based on the restored settings - this->switch_to_action_(compute_action_()); + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); this->setup_complete_ = true; this->publish_state(); } -float ThermostatClimate::hysteresis() { return this->hysteresis_; } + +float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; } +float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; } +float ThermostatClimate::heat_deadband() { return this->heating_deadband_; } +float ThermostatClimate::heat_overrun() { return this->heating_overrun_; } + void ThermostatClimate::refresh() { this->switch_to_mode_(this->mode); - this->switch_to_action_(compute_action_()); + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); this->switch_to_fan_mode_(this->fan_mode.value()); this->switch_to_swing_mode_(this->swing_mode); + this->check_temperature_change_trigger_(); this->publish_state(); } + +bool ThermostatClimate::climate_action_change_delayed() { + switch (this->compute_action_(true)) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + return !this->idle_action_ready_(); + case climate::CLIMATE_ACTION_COOLING: + return !this->cooling_action_ready_(); + case climate::CLIMATE_ACTION_HEATING: + return !this->heating_action_ready_(); + case climate::CLIMATE_ACTION_FAN: + return !this->fanning_action_ready_(); + case climate::CLIMATE_ACTION_DRYING: + return !this->drying_action_ready_(); + default: + break; + } + return false; +} + +bool ThermostatClimate::fan_mode_change_delayed() { return !this->fan_mode_ready_(); } + +climate::ClimateAction ThermostatClimate::delayed_climate_action() { return this->compute_action_(true); } + +climate::ClimateFanMode ThermostatClimate::delayed_fan_mode() { return this->desired_fan_mode_; } + +bool ThermostatClimate::hysteresis_valid() { + if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) && + (isnan(this->cooling_deadband_) || isnan(this->cooling_overrun_))) + return false; + + if (this->supports_heat_ && (isnan(this->heating_deadband_) || isnan(this->heating_overrun_))) + return false; + + return true; +} + +void ThermostatClimate::validate_target_temperature() { + if (isnan(this->target_temperature)) { + this->target_temperature = + ((this->get_traits().get_visual_max_temperature() - this->get_traits().get_visual_min_temperature()) / 2) + + this->get_traits().get_visual_min_temperature(); + } else { + // target_temperature must be between the visual minimum and the visual maximum + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } +} + +void ThermostatClimate::validate_target_temperatures() { + if (this->supports_two_points_) { + this->validate_target_temperature_low(); + this->validate_target_temperature_high(); + } else { + this->validate_target_temperature(); + } +} + +void ThermostatClimate::validate_target_temperature_low() { + if (isnan(this->target_temperature_low)) { + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + } else { + // target_temperature_low must not be lower than the visual minimum + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + // target_temperature_low must not be greater than the visual maximum minus set_point_minimum_differential_ + if (this->target_temperature_low > + this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_) + this->target_temperature_low = + this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_; + // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high + if (this->target_temperature_low > this->target_temperature_high - this->set_point_minimum_differential_) + this->target_temperature_high = this->target_temperature_low + this->set_point_minimum_differential_; + } +} + +void ThermostatClimate::validate_target_temperature_high() { + if (isnan(this->target_temperature_high)) { + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + } else { + // target_temperature_high must not be lower than the visual maximum + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + // target_temperature_high must not be lower than the visual minimum plus set_point_minimum_differential_ + if (this->target_temperature_high < + this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_) + this->target_temperature_high = + this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_; + // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low + if (this->target_temperature_high < this->target_temperature_low + this->set_point_minimum_differential_) + this->target_temperature_low = this->target_temperature_high - this->set_point_minimum_differential_; + } +} + void ThermostatClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - if (call.get_target_temperature_low().has_value()) - this->target_temperature_low = *call.get_target_temperature_low(); - if (call.get_target_temperature_high().has_value()) - this->target_temperature_high = *call.get_target_temperature_high(); if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); } else { this->preset = *call.get_preset(); - ; } } - // set point validation + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); if (this->supports_two_points_) { - if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) - this->target_temperature_low = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) - this->target_temperature_high = this->get_traits().get_visual_max_temperature(); - if (this->target_temperature_high < this->target_temperature_low) - this->target_temperature_high = this->target_temperature_low; + if (call.get_target_temperature_low().has_value()) { + this->target_temperature_low = *call.get_target_temperature_low(); + validate_target_temperature_low(); + } + if (call.get_target_temperature_high().has_value()) { + this->target_temperature_high = *call.get_target_temperature_high(); + validate_target_temperature_high(); + } } else { - if (this->target_temperature < this->get_traits().get_visual_min_temperature()) - this->target_temperature = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature > this->get_traits().get_visual_max_temperature()) - this->target_temperature = this->get_traits().get_visual_max_temperature(); + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + validate_target_temperature(); + } } // make any changes happen refresh(); } + climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); @@ -127,111 +236,111 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.set_supports_action(true); return traits; } -climate::ClimateAction ThermostatClimate::compute_action_() { - climate::ClimateAction target_action = this->action; - if (this->supports_two_points_) { - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || - isnan(this->target_temperature_high) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { - target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; - case climate::CLIMATE_MODE_HEAT_COOL: - case climate::CLIMATE_MODE_COOL: - case climate::CLIMATE_MODE_HEAT: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature_low - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } - } else { - if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { - target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; - case climate::CLIMATE_MODE_COOL: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - case climate::CLIMATE_MODE_HEAT: - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } +climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_timers) { + auto target_action = climate::CLIMATE_ACTION_IDLE; + // if any hysteresis values or current_temperature is not valid, we go to OFF; + if (isnan(this->current_temperature) || !this->hysteresis_valid()) { + return climate::CLIMATE_ACTION_OFF; + } + // do not change the action if an "ON" timer is running + if ((!ignore_timers) && + (timer_active_(thermostat::TIMER_IDLE_ON) || timer_active_(thermostat::TIMER_COOLING_ON) || + timer_active_(thermostat::TIMER_FANNING_ON) || timer_active_(thermostat::TIMER_HEATING_ON))) { + return this->action; + } + + // ensure set point(s) is/are valid before computing the action + this->validate_target_temperatures(); + // everything has been validated so we can now safely compute the action + switch (this->mode) { + // if the climate mode is OFF then the climate action must be OFF + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->fanning_required_()) + target_action = climate::CLIMATE_ACTION_FAN; + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (this->cooling_required_() && this->heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + case climate::CLIMATE_MODE_COOL: + if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + break; + case climate::CLIMATE_MODE_HEAT: + if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; + } + // do not abruptly switch actions. cycle through IDLE, first. we'll catch this at the next update. + if ((((this->action == climate::CLIMATE_ACTION_COOLING) || (this->action == climate::CLIMATE_ACTION_DRYING)) && + (target_action == climate::CLIMATE_ACTION_HEATING)) || + ((this->action == climate::CLIMATE_ACTION_HEATING) && + ((target_action == climate::CLIMATE_ACTION_COOLING) || (target_action == climate::CLIMATE_ACTION_DRYING)))) { + return climate::CLIMATE_ACTION_IDLE; } - // do not switch to an action that isn't enabled per the active climate mode - if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) - target_action = climate::CLIMATE_ACTION_IDLE; - if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) - target_action = climate::CLIMATE_ACTION_IDLE; return target_action; } + +climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { + auto target_action = climate::CLIMATE_ACTION_IDLE; + // if any hysteresis values or current_temperature is not valid, we go to OFF; + if (isnan(this->current_temperature) || !this->hysteresis_valid()) { + return climate::CLIMATE_ACTION_OFF; + } + + // ensure set point(s) is/are valid before computing the action + this->validate_target_temperatures(); + // everything has been validated so we can now safely compute the action + switch (this->mode) { + // if the climate mode is OFF then the climate action must be OFF + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (this->supplemental_cooling_required_() && this->supplemental_heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->supplemental_cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->supplemental_heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + case climate::CLIMATE_MODE_COOL: + if (this->supplemental_cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + break; + case climate::CLIMATE_MODE_HEAT: + if (this->supplemental_heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; + } + + return target_action; +} + void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->action) && this->setup_complete_) @@ -241,34 +350,81 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && this->setup_complete_) { - // switching from OFF to IDLE or vice-versa - // these only have visual difference. OFF means user manually disabled, - // IDLE means it's in auto mode but value is in target range. + // switching from OFF to IDLE or vice-versa -- this is only a visual difference. + // OFF means user manually disabled, IDLE means the temperature is in target range. this->action = action; return; } - if (this->prev_action_trigger_ != nullptr) { - this->prev_action_trigger_->stop_action(); - this->prev_action_trigger_ = nullptr; - } - Trigger<> *trig = this->idle_action_trigger_; + bool action_ready = false; + Trigger<> *trig = this->idle_action_trigger_, *trig_fan = nullptr; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - // trig = this->idle_action_trigger_; + if (this->idle_action_ready_()) { + this->start_timer_(thermostat::TIMER_IDLE_ON); + if (this->action == climate::CLIMATE_ACTION_COOLING) + this->start_timer_(thermostat::TIMER_COOLING_OFF); + if (this->action == climate::CLIMATE_ACTION_FAN) { + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + else + this->start_timer_(thermostat::TIMER_FANNING_OFF); + } + if (this->action == climate::CLIMATE_ACTION_HEATING) + this->start_timer_(thermostat::TIMER_HEATING_OFF); + // trig = this->idle_action_trigger_; + ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); + this->cooling_max_runtime_exceeded_ = false; + this->heating_max_runtime_exceeded_ = false; + action_ready = true; + } break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_action_trigger_; + if (this->cooling_action_ready_()) { + this->start_timer_(thermostat::TIMER_COOLING_ON); + this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (this->supports_fan_with_cooling_) { + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig_fan = this->fan_only_action_trigger_; + } + trig = this->cool_action_trigger_; + ESP_LOGVV(TAG, "Switching to COOLING action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_action_trigger_; + if (this->heating_action_ready_()) { + this->start_timer_(thermostat::TIMER_HEATING_ON); + this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (this->supports_fan_with_heating_) { + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig_fan = this->fan_only_action_trigger_; + } + trig = this->heat_action_trigger_; + ESP_LOGVV(TAG, "Switching to HEATING action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_FAN: - trig = this->fan_only_action_trigger_; + if (this->fanning_action_ready_()) { + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + else + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig = this->fan_only_action_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_DRYING: - trig = this->dry_action_trigger_; + if (this->drying_action_ready_()) { + this->start_timer_(thermostat::TIMER_COOLING_ON); + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig = this->dry_action_trigger_; + ESP_LOGVV(TAG, "Switching to DRYING action"); + action_ready = true; + } break; default: // we cannot report an invalid mode back to HA (even if it asked for one) @@ -276,62 +432,144 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { action = climate::CLIMATE_ACTION_OFF; // trig = this->idle_action_trigger_; } - assert(trig != nullptr); - trig->trigger(); - this->action = action; - this->prev_action_trigger_ = trig; + + if (action_ready) { + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop_action(); + this->prev_action_trigger_ = nullptr; + } + this->action = action; + this->prev_action_trigger_ = trig; + assert(trig != nullptr); + trig->trigger(); + // if enabled, call the fan_only action with cooling/heating actions + if (trig_fan != nullptr) { + ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action"); + trig_fan->trigger(); + } + } } + +void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->supplemental_action_) && this->setup_complete_) + // already in target mode + return; + + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + break; + case climate::CLIMATE_ACTION_COOLING: + this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + break; + case climate::CLIMATE_ACTION_HEATING: + this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + break; + default: + return; + } + ESP_LOGVV(TAG, "Updating supplemental action..."); + this->supplemental_action_ = action; + this->trigger_supplemental_action_(); +} + +void ThermostatClimate::trigger_supplemental_action_() { + Trigger<> *trig = nullptr; + + switch (this->supplemental_action_) { + case climate::CLIMATE_ACTION_COOLING: + if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + } + trig = this->supplemental_cool_action_trigger_; + ESP_LOGVV(TAG, "Calling supplemental COOLING action"); + break; + case climate::CLIMATE_ACTION_HEATING: + if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + } + trig = this->supplemental_heat_action_trigger_; + ESP_LOGVV(TAG, "Calling supplemental HEATING action"); + break; + default: + break; + } + + if (trig != nullptr) { + assert(trig != nullptr); + trig->trigger(); + } +} + void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { // setup_complete_ helps us ensure an action is called immediately after boot if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) // already in target mode return; - if (this->prev_fan_mode_trigger_ != nullptr) { - this->prev_fan_mode_trigger_->stop_action(); - this->prev_fan_mode_trigger_ = nullptr; + this->desired_fan_mode_ = fan_mode; // needed for timer callback + + if (this->fan_mode_ready_()) { + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_ON mode"); + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_OFF mode"); + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_AUTO mode"); + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_LOW mode"); + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode"); + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_HIGH mode"); + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode"); + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode"); + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop_action(); + this->prev_fan_mode_trigger_ = nullptr; + } + this->start_timer_(thermostat::TIMER_FAN_MODE); + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; } - Trigger<> *trig = this->fan_mode_auto_trigger_; - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - trig = this->fan_mode_on_trigger_; - break; - case climate::CLIMATE_FAN_OFF: - trig = this->fan_mode_off_trigger_; - break; - case climate::CLIMATE_FAN_AUTO: - // trig = this->fan_mode_auto_trigger_; - break; - case climate::CLIMATE_FAN_LOW: - trig = this->fan_mode_low_trigger_; - break; - case climate::CLIMATE_FAN_MEDIUM: - trig = this->fan_mode_medium_trigger_; - break; - case climate::CLIMATE_FAN_HIGH: - trig = this->fan_mode_high_trigger_; - break; - case climate::CLIMATE_FAN_MIDDLE: - trig = this->fan_mode_middle_trigger_; - break; - case climate::CLIMATE_FAN_FOCUS: - trig = this->fan_mode_focus_trigger_; - break; - case climate::CLIMATE_FAN_DIFFUSE: - trig = this->fan_mode_diffuse_trigger_; - break; - default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - fan_mode = climate::CLIMATE_FAN_AUTO; - // trig = this->fan_mode_auto_trigger_; - } - assert(trig != nullptr); - trig->trigger(); - this->fan_mode = fan_mode; - this->prev_fan_mode_ = fan_mode; - this->prev_fan_mode_trigger_ = trig; } + void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { // setup_complete_ helps us ensure an action is called immediately after boot if ((mode == this->prev_mode_) && this->setup_complete_) @@ -374,6 +612,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; } + void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { // setup_complete_ helps us ensure an action is called immediately after boot if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) @@ -410,6 +649,244 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; } + +bool ThermostatClimate::idle_action_ready_() { + if (this->supports_fan_only_action_uses_fan_mode_timer_) { + return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || + this->timer_active_(thermostat::TIMER_HEATING_ON)); + } + return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || + this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::cooling_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || + this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::drying_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || + this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } + +bool ThermostatClimate::fanning_action_ready_() { + if (this->supports_fan_only_action_uses_fan_mode_timer_) { + return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + } + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); +} + +bool ThermostatClimate::heating_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || + this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); +} + +void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { + if (this->timer_duration_(timer_index) > 0) { + this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + this->timer_cbf_(timer_index)); + this->timer_[timer_index].active = true; + } +} + +bool ThermostatClimate::cancel_timer_(ThermostatClimateTimerIndex timer_index) { + this->timer_[timer_index].active = false; + return this->cancel_timeout(this->timer_[timer_index].name); +} + +bool ThermostatClimate::timer_active_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].active; +} + +uint32_t ThermostatClimate::timer_duration_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].time; +} + +std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].func; +} + +void ThermostatClimate::cooling_max_run_time_timer_callback_() { + ESP_LOGVV(TAG, "cooling_max_run_time timer expired"); + this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].active = false; + this->cooling_max_runtime_exceeded_ = true; + this->trigger_supplemental_action_(); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::cooling_off_timer_callback_() { + ESP_LOGVV(TAG, "cooling_off timer expired"); + this->timer_[thermostat::TIMER_COOLING_OFF].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::cooling_on_timer_callback_() { + ESP_LOGVV(TAG, "cooling_on timer expired"); + this->timer_[thermostat::TIMER_COOLING_ON].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::fan_mode_timer_callback_() { + ESP_LOGVV(TAG, "fan_mode timer expired"); + this->timer_[thermostat::TIMER_FAN_MODE].active = false; + this->switch_to_fan_mode_(this->desired_fan_mode_); + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->switch_to_action_(this->compute_action_()); +} + +void ThermostatClimate::fanning_off_timer_callback_() { + ESP_LOGVV(TAG, "fanning_off timer expired"); + this->timer_[thermostat::TIMER_FANNING_OFF].active = false; + this->switch_to_action_(this->compute_action_()); +} + +void ThermostatClimate::fanning_on_timer_callback_() { + ESP_LOGVV(TAG, "fanning_on timer expired"); + this->timer_[thermostat::TIMER_FANNING_ON].active = false; + this->switch_to_action_(this->compute_action_()); +} + +void ThermostatClimate::heating_max_run_time_timer_callback_() { + ESP_LOGVV(TAG, "heating_max_run_time timer expired"); + this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].active = false; + this->heating_max_runtime_exceeded_ = true; + this->trigger_supplemental_action_(); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::heating_off_timer_callback_() { + ESP_LOGVV(TAG, "heating_off timer expired"); + this->timer_[thermostat::TIMER_HEATING_OFF].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::heating_on_timer_callback_() { + ESP_LOGVV(TAG, "heating_on timer expired"); + this->timer_[thermostat::TIMER_HEATING_ON].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::idle_on_timer_callback_() { + ESP_LOGVV(TAG, "idle_on timer expired"); + this->timer_[thermostat::TIMER_IDLE_ON].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::check_temperature_change_trigger_() { + if (this->supports_two_points_) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((this->prev_target_temperature_low_ == this->target_temperature_low) && + (this->prev_target_temperature_high_ == this->target_temperature_high) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperatures so we can check them again later; the trigger will fire below + this->prev_target_temperature_low_ = this->target_temperature_low; + this->prev_target_temperature_high_ = this->target_temperature_high; + } + } else { + if ((this->prev_target_temperature_ == this->target_temperature) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperature so we can check it again later; the trigger will fire below + this->prev_target_temperature_ = this->target_temperature; + } + } + // trigger the action + Trigger<> *trig = this->temperature_change_trigger_; + assert(trig != nullptr); + trig->trigger(); +} + +bool ThermostatClimate::cooling_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + + if (this->supports_cool_) { + if (this->current_temperature > temperature + this->cooling_deadband_) { + // if the current temperature exceeds the target + deadband, cooling is required + return true; + } else if (this->current_temperature < temperature - this->cooling_overrun_) { + // if the current temperature is less than the target - overrun, cooling should stop + return false; + } else { + // if we get here, the current temperature is between target + deadband and target - overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_COOLING) && + ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_COOL)); + } + } + return false; +} + +bool ThermostatClimate::fanning_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + + if (this->supports_fan_only_) { + if (this->supports_fan_only_cooling_) { + if (this->current_temperature > temperature + this->cooling_deadband_) { + // if the current temperature exceeds the target + deadband, fanning is required + return true; + } else if (this->current_temperature < temperature - this->cooling_overrun_) { + // if the current temperature is less than the target - overrun, fanning should stop + return false; + } else { + // if we get here, the current temperature is between target + deadband and target - overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_FAN) && (this->mode == climate::CLIMATE_MODE_FAN_ONLY); + } + } else { + return true; + } + } + return false; +} + +bool ThermostatClimate::heating_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; + + if (this->supports_heat_) { + if (this->current_temperature < temperature - this->heating_deadband_) { + // if the current temperature is below the target - deadband, heating is required + return true; + } else if (this->current_temperature > temperature + this->heating_overrun_) { + // if the current temperature is above the target + overrun, heating should stop + return false; + } else { + // if we get here, the current temperature is between target - deadband and target + overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_HEATING) && + ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_HEAT)); + } + } + return false; +} + +bool ThermostatClimate::supplemental_cooling_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + // the component must supports_cool_ and the climate action must be climate::CLIMATE_ACTION_COOLING. then... + // supplemental cooling is required if the max delta or max runtime was exceeded or the action is already engaged + return this->supports_cool_ && (this->action == climate::CLIMATE_ACTION_COOLING) && + (this->cooling_max_runtime_exceeded_ || + (this->current_temperature > temperature + this->supplemental_cool_delta_) || + (this->supplemental_action_ == climate::CLIMATE_ACTION_COOLING)); +} + +bool ThermostatClimate::supplemental_heating_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; + // the component must supports_heat_ and the climate action must be climate::CLIMATE_ACTION_HEATING. then... + // supplemental heating is required if the max delta or max runtime was exceeded or the action is already engaged + return this->supports_heat_ && (this->action == climate::CLIMATE_ACTION_HEATING) && + (this->heating_max_runtime_exceeded_ || + (this->current_temperature < temperature - this->supplemental_heat_delta_) || + (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); +} + void ThermostatClimate::change_away_(bool away) { if (!away) { if (this->supports_two_points_) { @@ -426,19 +903,24 @@ void ThermostatClimate::change_away_(bool away) { } this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } + void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; } + void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { this->supports_away_ = true; this->away_config_ = away_config; } + ThermostatClimate::ThermostatClimate() : cool_action_trigger_(new Trigger<>()), + supplemental_cool_action_trigger_(new Trigger<>()), cool_mode_trigger_(new Trigger<>()), dry_action_trigger_(new Trigger<>()), dry_mode_trigger_(new Trigger<>()), heat_action_trigger_(new Trigger<>()), + supplemental_heat_action_trigger_(new Trigger<>()), heat_mode_trigger_(new Trigger<>()), auto_mode_trigger_(new Trigger<>()), idle_action_trigger_(new Trigger<>()), @@ -457,9 +939,61 @@ ThermostatClimate::ThermostatClimate() swing_mode_both_trigger_(new Trigger<>()), swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), - swing_mode_vertical_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } + swing_mode_vertical_trigger_(new Trigger<>()), + temperature_change_trigger_(new Trigger<>()) {} + +void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } +void ThermostatClimate::set_set_point_minimum_differential(float differential) { + this->set_point_minimum_differential_ = differential; +} +void ThermostatClimate::set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; } +void ThermostatClimate::set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; } +void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; } +void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; } +void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } +void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } +void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FAN_MODE].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FANNING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FANNING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_IDLE_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; } @@ -467,6 +1001,19 @@ void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_a void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_fan_only_action_uses_fan_mode_timer( + bool supports_fan_only_action_uses_fan_mode_timer) { + this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer; +} +void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) { + this->supports_fan_only_cooling_ = supports_fan_only_cooling; +} +void ThermostatClimate::set_supports_fan_with_cooling(bool supports_fan_with_cooling) { + this->supports_fan_with_cooling_ = supports_fan_with_cooling; +} +void ThermostatClimate::set_supports_fan_with_heating(bool supports_fan_with_heating) { + this->supports_fan_with_heating_ = supports_fan_with_heating; +} void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { this->supports_fan_mode_on_ = supports_fan_mode_on; @@ -510,10 +1057,17 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod void ThermostatClimate::set_supports_two_points(bool supports_two_points) { this->supports_two_points_ = supports_two_points; } + Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const { + return this->supplemental_cool_action_trigger_; +} Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() const { + return this->supplemental_heat_action_trigger_; +} Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } @@ -534,6 +1088,8 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this- Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } + void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); if (this->supports_heat_) { @@ -542,18 +1098,62 @@ void ThermostatClimate::dump_config() { else ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); } - if ((this->supports_cool_) || (this->supports_fan_only_)) { + if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) { if (this->supports_two_points_) ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); else ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); } - ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); + ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + if (this->supports_cool_) { + ESP_LOGCONFIG(TAG, " Cooling Parameters:"); + ESP_LOGCONFIG(TAG, " Deadband: %.1f°C", this->cooling_deadband_); + ESP_LOGCONFIG(TAG, " Overrun: %.1f°C", this->cooling_overrun_); + if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, " Supplemental Delta: %.1f°C", this->supplemental_cool_delta_); + ESP_LOGCONFIG(TAG, " Maximum Run Time: %us", + this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000); + } + ESP_LOGCONFIG(TAG, " Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); + } + if (this->supports_heat_) { + ESP_LOGCONFIG(TAG, " Heating Parameters:"); + ESP_LOGCONFIG(TAG, " Deadband: %.1f°C", this->heating_deadband_); + ESP_LOGCONFIG(TAG, " Overrun: %.1f°C", this->heating_overrun_); + if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, " Supplemental Delta: %.1f°C", this->supplemental_heat_delta_); + ESP_LOGCONFIG(TAG, " Maximum Run Time: %us", + this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000); + } + ESP_LOGCONFIG(TAG, " Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); + } + if (this->supports_fan_only_) { + ESP_LOGCONFIG(TAG, " Fanning Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Fanning Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + } + if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || + this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || + this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_) { + ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %us", + this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); + } + ESP_LOGCONFIG(TAG, " Minimum Idle Time: %us", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports HEAT/COOL: %s", YESNO(this->supports_heat_cool_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s", + YESNO(this->supports_fan_only_action_uses_fan_mode_timer_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_COOLING: %s", YESNO(this->supports_fan_only_cooling_)); + if (this->supports_cool_) + ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); @@ -589,8 +1189,10 @@ void ThermostatClimate::dump_config() { } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) : default_temperature(default_temperature) {} + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 3fd482da53..1a5fd82ac0 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -8,6 +8,26 @@ namespace esphome { namespace thermostat { +enum ThermostatClimateTimerIndex : size_t { + TIMER_COOLING_MAX_RUN_TIME = 0, + TIMER_COOLING_OFF = 1, + TIMER_COOLING_ON = 2, + TIMER_FAN_MODE = 3, + TIMER_FANNING_OFF = 4, + TIMER_FANNING_ON = 5, + TIMER_HEATING_MAX_RUN_TIME = 6, + TIMER_HEATING_OFF = 7, + TIMER_HEATING_ON = 8, + TIMER_IDLE_ON = 9, +}; + +struct ThermostatClimateTimer { + const std::string name; + bool active; + uint32_t time; + std::function func; +}; + struct ThermostatClimateTargetTempConfig { public: ThermostatClimateTargetTempConfig(); @@ -17,7 +37,10 @@ struct ThermostatClimateTargetTempConfig { float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; - float hysteresis{NAN}; + float cool_deadband_{NAN}; + float cool_overrun_{NAN}; + float heat_deadband_{NAN}; + float heat_overrun_{NAN}; }; class ThermostatClimate : public climate::Climate, public Component { @@ -26,13 +49,35 @@ class ThermostatClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; - void set_hysteresis(float hysteresis); + void set_default_mode(climate::ClimateMode default_mode); + void set_set_point_minimum_differential(float differential); + void set_cool_deadband(float deadband); + void set_cool_overrun(float overrun); + void set_heat_deadband(float deadband); + void set_heat_overrun(float overrun); + void set_supplemental_cool_delta(float delta); + void set_supplemental_heat_delta(float delta); + void set_cooling_maximum_run_time_in_sec(uint32_t time); + void set_heating_maximum_run_time_in_sec(uint32_t time); + void set_cooling_minimum_off_time_in_sec(uint32_t time); + void set_cooling_minimum_run_time_in_sec(uint32_t time); + void set_fan_mode_minimum_switching_time_in_sec(uint32_t time); + void set_fanning_minimum_off_time_in_sec(uint32_t time); + void set_fanning_minimum_run_time_in_sec(uint32_t time); + void set_heating_minimum_off_time_in_sec(uint32_t time); + void set_heating_minimum_run_time_in_sec(uint32_t time); + void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); + void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); void set_supports_cool(bool supports_cool); void set_supports_dry(bool supports_dry); void set_supports_fan_only(bool supports_fan_only); + void set_supports_fan_only_action_uses_fan_mode_timer(bool fan_only_action_uses_fan_mode_timer); + void set_supports_fan_only_cooling(bool supports_fan_only_cooling); + void set_supports_fan_with_cooling(bool supports_fan_with_cooling); + void set_supports_fan_with_heating(bool supports_fan_with_heating); void set_supports_heat(bool supports_heat); void set_supports_fan_mode_on(bool supports_fan_mode_on); void set_supports_fan_mode_off(bool supports_fan_mode_off); @@ -53,9 +98,11 @@ class ThermostatClimate : public climate::Climate, public Component { void set_away_config(const ThermostatClimateTargetTempConfig &away_config); Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_supplemental_cool_action_trigger() const; Trigger<> *get_dry_action_trigger() const; Trigger<> *get_fan_only_action_trigger() const; Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_supplemental_heat_action_trigger() const; Trigger<> *get_idle_action_trigger() const; Trigger<> *get_auto_mode_trigger() const; Trigger<> *get_cool_mode_trigger() const; @@ -76,10 +123,27 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_horizontal_trigger() const; Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; - /// Get current hysteresis value - float hysteresis(); + Trigger<> *get_temperature_change_trigger() const; + /// Get current hysteresis values + float cool_deadband(); + float cool_overrun(); + float heat_deadband(); + float heat_overrun(); /// Call triggers based on updated climate states (modes/actions) void refresh(); + /// Returns true if a climate action/fan mode transition is being delayed + bool climate_action_change_delayed(); + bool fan_mode_change_delayed(); + /// Returns the climate action that is being delayed (check climate_action_change_delayed(), first!) + climate::ClimateAction delayed_climate_action(); + /// Returns the fan mode that is being delayed (check fan_mode_change_delayed(), first!) + climate::ClimateFanMode delayed_fan_mode(); + /// Set point and hysteresis validation + bool hysteresis_valid(); // returns true if valid + void validate_target_temperature(); + void validate_target_temperatures(); + void validate_target_temperature_low(); + void validate_target_temperature_high(); protected: /// Override control to change settings of the climate device. @@ -92,10 +156,13 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateTraits traits() override; /// Re-compute the required action of this climate controller. - climate::ClimateAction compute_action_(); + climate::ClimateAction compute_action_(bool ignore_timers = false); + climate::ClimateAction compute_supplemental_action_(); /// Switch the climate device to the given climate action. void switch_to_action_(climate::ClimateAction action); + void switch_to_supplemental_action_(climate::ClimateAction action); + void trigger_supplemental_action_(); /// Switch the climate device to the given climate fan mode. void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); @@ -106,6 +173,43 @@ class ThermostatClimate : public climate::Climate, public Component { /// Switch the climate device to the given climate swing mode. void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + /// Check if the temperature change trigger should be called. + void check_temperature_change_trigger_(); + + /// Is the action ready to be called? Returns true if so + bool idle_action_ready_(); + bool cooling_action_ready_(); + bool drying_action_ready_(); + bool fan_mode_ready_(); + bool fanning_action_ready_(); + bool heating_action_ready_(); + + /// Start/cancel/get status of climate action timer + void start_timer_(ThermostatClimateTimerIndex timer_index); + bool cancel_timer_(ThermostatClimateTimerIndex timer_index); + bool timer_active_(ThermostatClimateTimerIndex timer_index); + uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index); + std::function timer_cbf_(ThermostatClimateTimerIndex timer_index); + + /// set_timeout() callbacks for various actions (see above) + void cooling_max_run_time_timer_callback_(); + void cooling_off_timer_callback_(); + void cooling_on_timer_callback_(); + void fan_mode_timer_callback_(); + void fanning_off_timer_callback_(); + void fanning_on_timer_callback_(); + void heating_max_run_time_timer_callback_(); + void heating_off_timer_callback_(); + void heating_on_timer_callback_(); + void idle_on_timer_callback_(); + + /// Check if cooling/fanning/heating actions are required; returns true if so + bool cooling_required_(); + bool fanning_required_(); + bool heating_required_(); + bool supplemental_cooling_required_(); + bool supplemental_heating_required_(); + /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -119,6 +223,13 @@ class ThermostatClimate : public climate::Climate, public Component { bool supports_dry_{false}; bool supports_fan_only_{false}; bool supports_heat_{false}; + /// Special flag -- enables fan_modes to share timer with fan_only climate action + bool supports_fan_only_action_uses_fan_mode_timer_{false}; + /// Special flag -- enables fan to be switched based on target_temperature_high + bool supports_fan_only_cooling_{false}; + /// Special flags -- enables fan_only action to be called with cooling/heating actions + bool supports_fan_with_cooling_{false}; + bool supports_fan_with_heating_{false}; /// Whether the controller supports turning on or off just the fan. /// @@ -161,12 +272,23 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value means that the controller has no such mode. bool supports_away_{false}; + /// Flags indicating if maximum allowable run time was exceeded + bool cooling_max_runtime_exceeded_{false}; + bool heating_max_runtime_exceeded_{false}; + + /// Used to start "off" delay timers at boot + bool use_startup_delay_{false}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action /// For example electric heat, where only heating (power on) and not-heating /// (power off) is possible. Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *supplemental_cool_action_trigger_{nullptr}; Trigger<> *cool_mode_trigger_{nullptr}; /// The trigger to call when the controller should switch to dry (dehumidification) mode. @@ -182,6 +304,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// For example window blinds, where only cooling (blinds closed) and not-cooling /// (blinds open) is possible. Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *supplemental_heat_action_trigger_{nullptr}; Trigger<> *heat_mode_trigger_{nullptr}; /// The trigger to call when the controller should switch to auto mode. @@ -242,6 +365,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the controller should switch the swing mode to "vertical". Trigger<> *swing_mode_vertical_trigger_{nullptr}; + /// The trigger to call when the target temperature(s) change(es). + Trigger<> *temperature_change_trigger_{nullptr}; + /// A reference to the trigger that was previously active. /// /// This is so that the previous trigger can be stopped before enabling a new one @@ -251,22 +377,57 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; + /// Desired fan_mode -- used to store desired mode for callback when switching is delayed + climate::ClimateFanMode desired_fan_mode_{climate::CLIMATE_FAN_ON}; + /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called + climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; + + /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; + /// Temperature data for normal/home and away modes ThermostatClimateTargetTempConfig normal_config_{}; ThermostatClimateTargetTempConfig away_config_{}; - /// Hysteresis value used for computing climate actions - float hysteresis_{0}; - - /// setup_complete_ blocks modifying/resetting the temps immediately after boot - bool setup_complete_{false}; + /// Climate action timers + std::vector timer_{ + {"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, + {"cool_off", false, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, + {"cool_on", false, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, + {"fan_mode", false, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, + {"fan_off", false, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, + {"fan_on", false, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, + {"heat_run", false, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, + {"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, + {"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, + {"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}}; }; } // namespace thermostat diff --git a/esphome/components/tlc5947/__init__.py b/esphome/components/tlc5947/__init__.py new file mode 100644 index 0000000000..84380bdace --- /dev/null +++ b/esphome/components/tlc5947/__init__.py @@ -0,0 +1,50 @@ +# this component is for the "TLC5947 24-Channel, 12-Bit PWM LED Driver" [https://www.ti.com/lit/ds/symlink/tlc5947.pdf], +# which is used e.g. on [https://www.adafruit.com/product/1429]. The code is based on the components sm2135 and sm26716. + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, + CONF_NUM_CHIPS, +) + +CONF_LAT_PIN = "lat_pin" +CONF_OE_PIN = "oe_pin" + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@rnauber"] + +tlc5947_ns = cg.esphome_ns.namespace("tlc5947") +TLC5947 = tlc5947_ns.class_("TLC5947", cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TLC5947), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LAT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + lat = await cg.gpio_pin_expression(config[CONF_LAT_PIN]) + cg.add(var.set_lat_pin(lat)) + if CONF_OE_PIN in config: + outenable = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_outenable_pin(outenable)) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) diff --git a/esphome/components/tlc5947/output.py b/esphome/components/tlc5947/output.py new file mode 100644 index 0000000000..ece47fa63d --- /dev/null +++ b/esphome/components/tlc5947/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import TLC5947 + +DEPENDENCIES = ["tlc5947"] +CODEOWNERS = ["@rnauber"] + +Channel = TLC5947.class_("Channel", output.FloatOutput) + +CONF_TLC5947_ID = "tlc5947_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_TLC5947_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/tlc5947/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp new file mode 100644 index 0000000000..a7e08c8341 --- /dev/null +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -0,0 +1,65 @@ +#include "tlc5947.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tlc5947 { + +static const char *const TAG = "tlc5947"; + +void TLC5947::setup() { + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->lat_pin_->setup(); + this->lat_pin_->digital_write(true); + if (this->outenable_pin_ != nullptr) { + this->outenable_pin_->setup(); + this->outenable_pin_->digital_write(false); + } + + this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); + + ESP_LOGCONFIG(TAG, "Done setting up TLC5947 output component."); +} +void TLC5947::dump_config() { + ESP_LOGCONFIG(TAG, "TLC5947:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + LOG_PIN(" LAT Pin: ", this->lat_pin_); + if (this->outenable_pin_ != nullptr) + LOG_PIN(" OE Pin: ", this->outenable_pin_); + ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); +} + +void TLC5947::loop() { + if (!this->update_) + return; + + this->lat_pin_->digital_write(false); + + // push the data out, MSB first, 12 bit word per channel, 24 channels per chip + for (int32_t ch = N_CHANNELS_PER_CHIP * num_chips_ - 1; ch >= 0; ch--) { + uint16_t word = pwm_amounts_[ch]; + for (uint8_t bit = 0; bit < 12; bit++) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(word & 0x800); + word <<= 1; + + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(true); // TWH0>12ns, so we should be fine using this as delay + } + } + + this->clock_pin_->digital_write(false); + + // latch the values, so they will be applied + this->lat_pin_->digital_write(true); + delayMicroseconds(1); // TWH1 > 30ns + this->lat_pin_->digital_write(false); + + this->update_ = false; +} + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tlc5947/tlc5947.h b/esphome/components/tlc5947/tlc5947.h new file mode 100644 index 0000000000..b608b861e7 --- /dev/null +++ b/esphome/components/tlc5947/tlc5947.h @@ -0,0 +1,69 @@ +#pragma once +// TLC5947 24-Channel, 12-Bit PWM LED Driver +// https://www.ti.com/lit/ds/symlink/tlc5947.pdf + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace tlc5947 { + +class TLC5947 : public Component { + public: + class Channel; + + const uint8_t N_CHANNELS_PER_CHIP = 24; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_lat_pin(GPIOPin *lat_pin) { lat_pin_ = lat_pin; } + void set_outenable_pin(GPIOPin *outenable_pin) { outenable_pin_ = outenable_pin; } + void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(TLC5947 *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0xfff); + this->parent_->set_channel_value_(this->channel_, amount); + } + + TLC5947 *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint16_t channel, uint16_t value) { + if (channel >= this->num_chips_ * N_CHANNELS_PER_CHIP) + return; + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + } + this->pwm_amounts_[channel] = value; + } + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *lat_pin_; + GPIOPin *outenable_pin_{nullptr}; + uint8_t num_chips_; + + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py index b54d5646ba..c5ffbb8df5 100644 --- a/esphome/components/tmp102/sensor.py +++ b/esphome/components/tmp102/sensor.py @@ -13,7 +13,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -28,7 +27,10 @@ TMP102Component = tmp102_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index a5fc027b20..054864dd83 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -5,12 +5,12 @@ from esphome.const import ( CONF_ID, CONF_UPDATE_INTERVAL, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] tmp117_ns = cg.esphome_ns.namespace("tmp117") TMP117Component = tmp117_ns.class_( @@ -19,7 +19,10 @@ TMP117Component = tmp117_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py index 2110cbfcf8..2d3add2399 100644 --- a/esphome/components/tof10120/sensor.py +++ b/esphome/components/tof10120/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -19,11 +18,10 @@ TOF10120Sensor = tof10120_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)}) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 95c9f1f127..3f2c644c87 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -1,16 +1,25 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate_ir -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_MODEL AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@kbx81"] toshiba_ns = cg.esphome_ns.namespace("toshiba") ToshibaClimate = toshiba_ns.class_("ToshibaClimate", climate_ir.ClimateIR) +Model = toshiba_ns.enum("Model") +MODELS = { + "GENERIC": Model.MODEL_GENERIC, + "RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C, + "RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F, +} + CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(ToshibaClimate), + cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True), } ) @@ -18,3 +27,4 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.register_climate_ir(var, config) + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index c08ae898b5..81ed5ddce4 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -3,13 +3,23 @@ namespace esphome { namespace toshiba { +struct RacPt1411hwruFanSpeed { + uint8_t code1; + uint8_t code2; +}; + +static const char *const TAG = "toshiba.climate"; +// Timings for IR bits/data const uint16_t TOSHIBA_HEADER_MARK = 4380; const uint16_t TOSHIBA_HEADER_SPACE = 4370; const uint16_t TOSHIBA_GAP_SPACE = 5480; +const uint16_t TOSHIBA_PACKET_SPACE = 10500; const uint16_t TOSHIBA_BIT_MARK = 540; const uint16_t TOSHIBA_ZERO_SPACE = 540; const uint16_t TOSHIBA_ONE_SPACE = 1620; - +const uint16_t TOSHIBA_CARRIER_FREQUENCY = 38000; +const uint8_t TOSHIBA_HEADER_LENGTH = 4; +// Generic Toshiba commands/flags const uint8_t TOSHIBA_COMMAND_DEFAULT = 0x01; const uint8_t TOSHIBA_COMMAND_TIMER = 0x02; const uint8_t TOSHIBA_COMMAND_POWER = 0x08; @@ -36,36 +46,122 @@ const uint8_t TOSHIBA_POWER_ECO = 0x03; const uint8_t TOSHIBA_MOTION_SWING = 0x04; const uint8_t TOSHIBA_MOTION_FIX = 0x00; -static const char *const TAG = "toshiba.climate"; +// RAC-PT1411HWRU temperature code flag bits +const uint8_t RAC_PT1411HWRU_FLAG_FAH = 0x01; +const uint8_t RAC_PT1411HWRU_FLAG_FRAC = 0x20; +const uint8_t RAC_PT1411HWRU_FLAG_NEG = 0x10; +// RAC-PT1411HWRU temperature short code flags mask +const uint8_t RAC_PT1411HWRU_FLAG_MASK = 0x0F; +// RAC-PT1411HWRU Headers, Footers and such +const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER0 = 0xB2; +const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER1 = 0xD5; +const uint8_t RAC_PT1411HWRU_MESSAGE_LENGTH = 6; +// RAC-PT1411HWRU "Comfort Sense" feature bits +const uint8_t RAC_PT1411HWRU_CS_ENABLED = 0x40; +const uint8_t RAC_PT1411HWRU_CS_DATA = 0x80; +const uint8_t RAC_PT1411HWRU_CS_HEADER = 0xBA; +const uint8_t RAC_PT1411HWRU_CS_FOOTER_AUTO = 0x7A; +const uint8_t RAC_PT1411HWRU_CS_FOOTER_COOL = 0x72; +const uint8_t RAC_PT1411HWRU_CS_FOOTER_HEAT = 0x7E; +// RAC-PT1411HWRU Swing +const uint8_t RAC_PT1411HWRU_SWING_HEADER = 0xB9; +const std::vector RAC_PT1411HWRU_SWING_VERTICAL{0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB}; +const std::vector RAC_PT1411HWRU_SWING_OFF{0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA}; +// RAC-PT1411HWRU Fan speeds +const uint8_t RAC_PT1411HWRU_FAN_OFF = 0x7B; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_AUTO{0xBF, 0x66}; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_LOW{0x9F, 0x28}; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_MED{0x5F, 0x3C}; +constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_HIGH{0x3F, 0x64}; +// RAC-PT1411HWRU Fan speed for Auto and Dry climate modes +const RacPt1411hwruFanSpeed RAC_PT1411HWRU_NO_FAN{0x1F, 0x65}; +// RAC-PT1411HWRU Modes +const uint8_t RAC_PT1411HWRU_MODE_AUTO = 0x08; +const uint8_t RAC_PT1411HWRU_MODE_COOL = 0x00; +const uint8_t RAC_PT1411HWRU_MODE_DRY = 0x04; +const uint8_t RAC_PT1411HWRU_MODE_FAN = 0x04; +const uint8_t RAC_PT1411HWRU_MODE_HEAT = 0x0C; +const uint8_t RAC_PT1411HWRU_MODE_OFF = 0x00; +// RAC-PT1411HWRU Fan-only "temperature"/system off +const uint8_t RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY = 0x0E; +// RAC-PT1411HWRU temperature codes are not sequential; they instead follow a modified Gray code. +// Hence these look-up tables. In addition, the upper nibble is used here for additional +// "negative" and "fractional value" flags as required for some temperatures. +// RAC-PT1411HWRU °C Temperatures (short codes) +const std::vector RAC_PT1411HWRU_TEMPERATURE_C{0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05, + 0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B}; +// RAC-PT1411HWRU °F Temperatures (short codes) +const std::vector RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02, + 0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C, + 0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B}; + +void ToshibaClimate::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + this->transmit_rac_pt1411hwru_temp_(); + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + // initialize target temperature to some value so that it's not NAN + this->target_temperature = + roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + this->fan_mode = climate::CLIMATE_FAN_AUTO; + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + // Set supported modes & temperatures based on model + this->minimum_temperature_ = this->temperature_min_(); + this->maximum_temperature_ = this->temperature_max_(); + this->supports_dry_ = this->toshiba_supports_dry_(); + this->supports_fan_only_ = this->toshiba_supports_fan_only_(); + this->fan_modes_ = this->toshiba_fan_modes_(); + this->swing_modes_ = this->toshiba_swing_modes_(); + // Never send nan to HA + if (isnan(this->target_temperature)) + this->target_temperature = 24; +} void ToshibaClimate::transmit_state() { + if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) { + transmit_rac_pt1411hwru_(); + } else { + transmit_generic_(); + } +} + +void ToshibaClimate::transmit_generic_() { uint8_t message[16] = {0}; uint8_t message_length = 9; - /* Header */ + // Header message[0] = 0xf2; message[1] = 0x0d; - /* Message length */ + // Message length message[2] = message_length - 6; - /* First checksum */ + // First checksum message[3] = message[0] ^ message[1] ^ message[2]; - /* Command */ + // Command message[4] = TOSHIBA_COMMAND_DEFAULT; - /* Temperature */ - uint8_t temperature = static_cast(this->target_temperature); - if (temperature < 17) { - temperature = 17; - } - if (temperature > 30) { - temperature = 30; - } - message[5] = (temperature - 17) << 4; + // Temperature + uint8_t temperature = static_cast( + clamp(this->target_temperature, TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX)); + message[5] = (temperature - static_cast(TOSHIBA_GENERIC_TEMP_C_MIN)) << 4; - /* Mode and fan */ + // Mode and fan uint8_t mode; switch (this->mode) { case climate::CLIMATE_MODE_OFF: @@ -87,28 +183,504 @@ void ToshibaClimate::transmit_state() { message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; - /* Zero */ + // Zero message[7] = 0x00; - /* If timers bit in the command is set, two extra bytes are added here */ + // If timers bit in the command is set, two extra bytes are added here - /* If power bit is set in the command, one extra byte is added here */ + // If power bit is set in the command, one extra byte is added here - /* The last byte is the xor of all bytes from [4] */ + // The last byte is the xor of all bytes from [4] for (uint8_t i = 4; i < 8; i++) { message[8] ^= message[i]; } - /* Transmit */ + // Transmit auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); - data->set_carrier_frequency(38000); - for (uint8_t copy = 0; copy < 2; copy++) { - data->mark(TOSHIBA_HEADER_MARK); - data->space(TOSHIBA_HEADER_SPACE); + encode_(data, message, message_length, 1); - for (uint8_t byte = 0; byte < message_length; byte++) { + transmit.perform(); +} + +void ToshibaClimate::transmit_rac_pt1411hwru_() { + uint8_t code = 0, index = 0, message[RAC_PT1411HWRU_MESSAGE_LENGTH * 2] = {0}; + float temperature = + clamp(this->target_temperature, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX); + float temp_adjd = temperature - TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + // Byte 0: Header upper (0xB2) + message[0] = RAC_PT1411HWRU_MESSAGE_HEADER0; + // Byte 1: Header lower (0x4D) + message[1] = ~message[0]; + // Byte 2u: Fan speed + // Byte 2l: 1111 (on) or 1011 (off) + if (this->mode == climate::CLIMATE_MODE_OFF) { + message[2] = RAC_PT1411HWRU_FAN_OFF; + } else if ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_DRY)) { + message[2] = RAC_PT1411HWRU_NO_FAN.code1; + message[7] = RAC_PT1411HWRU_NO_FAN.code2; + } else { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + message[2] = RAC_PT1411HWRU_FAN_LOW.code1; + message[7] = RAC_PT1411HWRU_FAN_LOW.code2; + break; + + case climate::CLIMATE_FAN_MEDIUM: + message[2] = RAC_PT1411HWRU_FAN_MED.code1; + message[7] = RAC_PT1411HWRU_FAN_MED.code2; + break; + + case climate::CLIMATE_FAN_HIGH: + message[2] = RAC_PT1411HWRU_FAN_HIGH.code1; + message[7] = RAC_PT1411HWRU_FAN_HIGH.code2; + break; + + case climate::CLIMATE_FAN_AUTO: + default: + message[2] = RAC_PT1411HWRU_FAN_AUTO.code1; + message[7] = RAC_PT1411HWRU_FAN_AUTO.code2; + } + } + // Byte 3u: ~Fan speed + // Byte 3l: 0000 (on) or 0100 (off) + message[3] = ~message[2]; + // Byte 4u: Temp + if (this->model_ == MODEL_RAC_PT1411HWRU_F) { + temperature = (temperature * 1.8) + 32; + temp_adjd = temperature - TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN; + } + + index = static_cast(roundf(temp_adjd)); + + if (this->model_ == MODEL_RAC_PT1411HWRU_F) { + code = RAC_PT1411HWRU_TEMPERATURE_F[index]; + message[9] |= RAC_PT1411HWRU_FLAG_FAH; + } else { + code = RAC_PT1411HWRU_TEMPERATURE_C[index]; + } + if ((this->mode == climate::CLIMATE_MODE_FAN_ONLY) || (this->mode == climate::CLIMATE_MODE_OFF)) { + code = RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY; + } + + if (code & RAC_PT1411HWRU_FLAG_FRAC) { + message[8] |= RAC_PT1411HWRU_FLAG_FRAC; + } + if (code & RAC_PT1411HWRU_FLAG_NEG) { + message[9] |= RAC_PT1411HWRU_FLAG_NEG; + } + message[4] = (code & RAC_PT1411HWRU_FLAG_MASK) << 4; + // Byte 4l: Mode + switch (this->mode) { + case climate::CLIMATE_MODE_OFF: + // zerooooo + break; + + case climate::CLIMATE_MODE_HEAT: + message[4] |= RAC_PT1411HWRU_MODE_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + message[4] |= RAC_PT1411HWRU_MODE_COOL; + break; + + case climate::CLIMATE_MODE_DRY: + message[4] |= RAC_PT1411HWRU_MODE_DRY; + break; + + case climate::CLIMATE_MODE_FAN_ONLY: + message[4] |= RAC_PT1411HWRU_MODE_FAN; + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + default: + message[4] |= RAC_PT1411HWRU_MODE_AUTO; + } + + // Byte 5u: ~Temp + // Byte 5l: ~Mode + message[5] = ~message[4]; + + if (this->mode != climate::CLIMATE_MODE_OFF) { + // Byte 6: Header (0xD5) + message[6] = RAC_PT1411HWRU_MESSAGE_HEADER1; + // Byte 7: Fan speed part 2 (done above) + // Byte 8: 0x20 for °F frac, else 0 (done above) + // Byte 9: 0x10=NEG, 0x01=°F (done above) + // Byte 10: 0 + // Byte 11: Checksum (bytes 6 through 10) + for (index = 6; index <= 10; index++) { + message[11] += message[index]; + } + } + ESP_LOGV(TAG, "*** Generated codes: 0x%.2X%.2X%.2X%.2X%.2X%.2X 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], + message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9], message[10], + message[11]); + + // load first block of IR code and repeat it once + encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + // load second block of IR code, if present + if (message[6] != 0) { + encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0); + } + + transmit.perform(); + + // Swing Mode + data->reset(); + data->space(TOSHIBA_PACKET_SPACE); + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + break; + + case climate::CLIMATE_SWING_OFF: + default: + encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + } + + data->space(TOSHIBA_PACKET_SPACE); + transmit.perform(); + + if (this->sensor_) { + transmit_rac_pt1411hwru_temp_(true, false); + } +} + +void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bool cs_send_update) { + if ((this->mode == climate::CLIMATE_MODE_HEAT) || (this->mode == climate::CLIMATE_MODE_COOL) || + (this->mode == climate::CLIMATE_MODE_HEAT_COOL)) { + uint8_t message[RAC_PT1411HWRU_MESSAGE_LENGTH] = {0}; + float temperature = clamp(this->current_temperature, 0.0, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX + 1); + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + // "Comfort Sense" feature notes + // IR Code: 0xBA45 xxXX yyYY + // xx: Temperature in °C + // Bit 6: feature state (on/off) + // Bit 7: message contains temperature data for feature (bit 6 must also be set) + // XX: Bitwise complement of xx + // yy: Mode: Auto=0x7A, Cool=0x72, Heat=0x7E + // YY: Bitwise complement of yy + // + // Byte 0: Header upper (0xBA) + message[0] = RAC_PT1411HWRU_CS_HEADER; + // Byte 1: Header lower (0x45) + message[1] = ~message[0]; + // Byte 2: Temperature in °C + message[2] = static_cast(roundf(temperature)); + if (cs_send_update) { + message[2] |= RAC_PT1411HWRU_CS_ENABLED | RAC_PT1411HWRU_CS_DATA; + } else if (cs_state) { + message[2] |= RAC_PT1411HWRU_CS_ENABLED; + } + // Byte 3: Bitwise complement of byte 2 + message[3] = ~message[2]; + // Byte 4: Footer upper + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + message[4] = RAC_PT1411HWRU_CS_FOOTER_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + message[4] = RAC_PT1411HWRU_CS_FOOTER_COOL; + break; + + case climate::CLIMATE_MODE_HEAT_COOL: + message[4] = RAC_PT1411HWRU_CS_FOOTER_AUTO; + + default: + break; + } + // Byte 5: Footer lower/bitwise complement of byte 4 + message[5] = ~message[4]; + + ESP_LOGV(TAG, "*** Generated code: 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], message[2], message[3], + message[4], message[5]); + // load IR code and repeat it once + encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1); + + transmit.perform(); + } +} + +uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) { + const std::vector header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, + RAC_PT1411HWRU_SWING_HEADER}; + + for (auto i : header) { + if ((message[0] == i) && (message[1] == static_cast(~i))) + return i; + } + if (message[0] == RAC_PT1411HWRU_MESSAGE_HEADER1) + return RAC_PT1411HWRU_MESSAGE_HEADER1; + + return 0; +} + +bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2) { + for (uint8_t i = 0; i < RAC_PT1411HWRU_MESSAGE_LENGTH; i++) { + if (message1[i] != message2[i]) + return false; + } + return true; +} + +bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) { + uint8_t checksum = 0; + + switch (is_valid_rac_pt1411hwru_header_(message)) { + case RAC_PT1411HWRU_MESSAGE_HEADER0: + case RAC_PT1411HWRU_CS_HEADER: + case RAC_PT1411HWRU_SWING_HEADER: + if (is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast(~message[3])) && + (message[4] == static_cast(~message[5]))) { + return true; + } + break; + + case RAC_PT1411HWRU_MESSAGE_HEADER1: + for (uint8_t i = 0; i < RAC_PT1411HWRU_MESSAGE_LENGTH - 1; i++) { + checksum += message[i]; + } + if (checksum == message[RAC_PT1411HWRU_MESSAGE_LENGTH - 1]) { + return true; + } + break; + + default: + return false; + } + + return false; +} + +bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t message[18] = {0}; + uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0; + + // Validate header + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + // Read incoming bits into buffer + if (!decode_(&data, message, message_length)) { + return false; + } + // Determine incoming message protocol version and/or length + if (is_valid_rac_pt1411hwru_header_(message)) { + // We already received four bytes + message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4; + } else if ((message[0] ^ message[1] ^ message[2]) != message[3]) { + // Return false if first checksum was not valid + return false; + } else { + // First checksum was valid so continue receiving the remaining bits + message_length = message[2] + 2; + } + // Decode the remaining bytes + if (!decode_(&data, &message[4], message_length)) { + return false; + } + // If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet + if (is_valid_rac_pt1411hwru_header_(message)) { + // There is always a space between packets + if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) { + return false; + } + // Validate header 2 + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + if (!decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) { + return false; + } + // If this is a RAC-PT1411HWRU message, there may also be a third packet. + // We do not fail the receive if we don't get this; it isn't always present + if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) { + // Validate header 3 + data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE); + if (decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) { + if (!is_valid_rac_pt1411hwru_message_(&message[12])) { + // If a third packet was received but the checksum is not valid, fail + return false; + } + } + } + if (!compare_rac_pt1411hwru_packets_(&message[0], &message[6])) { + // If the first two packets don't match each other, fail + return false; + } + if (!is_valid_rac_pt1411hwru_message_(&message[0])) { + // If the first packet isn't valid, fail + return false; + } + } + + // Header has been verified, now determine protocol version and set the climate component properties + switch (is_valid_rac_pt1411hwru_header_(message)) { + // Power, temperature, mode, fan speed + case RAC_PT1411HWRU_MESSAGE_HEADER0: + // Get the mode + switch (message[4] & 0x0F) { + case RAC_PT1411HWRU_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + + // case RAC_PT1411HWRU_MODE_OFF: + case RAC_PT1411HWRU_MODE_COOL: + if (((message[4] >> 4) == RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY) && (message[2] == RAC_PT1411HWRU_FAN_OFF)) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + this->mode = climate::CLIMATE_MODE_COOL; + } + break; + + // case RAC_PT1411HWRU_MODE_DRY: + case RAC_PT1411HWRU_MODE_FAN: + if ((message[4] >> 4) == RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY) + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + else + this->mode = climate::CLIMATE_MODE_DRY; + break; + + case RAC_PT1411HWRU_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + default: + this->mode = climate::CLIMATE_MODE_OFF; + break; + } + // Get the fan speed/mode + switch (message[2]) { + case RAC_PT1411HWRU_FAN_LOW.code1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + + case RAC_PT1411HWRU_FAN_MED.code1: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + + case RAC_PT1411HWRU_FAN_HIGH.code1: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + + case RAC_PT1411HWRU_FAN_AUTO.code1: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + // Get the target temperature + if (is_valid_rac_pt1411hwru_message_(&message[12])) { + temperature_code = + (message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG); + if (message[15] & RAC_PT1411HWRU_FLAG_FAH) { + for (uint8_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_F.size(); i++) { + if (RAC_PT1411HWRU_TEMPERATURE_F[i] == temperature_code) { + this->target_temperature = static_cast((i + TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN - 32) * 5) / 9; + } + } + } else { + for (uint8_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_C.size(); i++) { + if (RAC_PT1411HWRU_TEMPERATURE_C[i] == temperature_code) { + this->target_temperature = i + TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + } + } + } + } + break; + // "Comfort Sense" temperature packet + case RAC_PT1411HWRU_CS_HEADER: + // "Comfort Sense" feature notes + // IR Code: 0xBA45 xxXX yyYY + // xx: Temperature in °C + // Bit 6: feature state (on/off) + // Bit 7: message contains temperature data for feature (bit 6 must also be set) + // XX: Bitwise complement of xx + // yy: Mode: Auto: 7A + // Cool: 72 + // Heat: 7E + // YY: Bitwise complement of yy + if ((message[2] & RAC_PT1411HWRU_CS_ENABLED) && (message[2] & RAC_PT1411HWRU_CS_DATA)) { + // Setting current_temperature this way allows the unit's remote to provide the temperature to HA + this->current_temperature = message[2] & ~(RAC_PT1411HWRU_CS_ENABLED | RAC_PT1411HWRU_CS_DATA); + } + break; + // Swing mode + case RAC_PT1411HWRU_SWING_HEADER: + if (message[4] == RAC_PT1411HWRU_SWING_VERTICAL[4]) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + break; + // Generic (old) Toshiba packet + default: + uint8_t checksum = 0; + // Add back the length of the header (we pruned it above) + message_length += TOSHIBA_HEADER_LENGTH; + // Validate the second checksum before trusting any more of the message + for (uint8_t i = TOSHIBA_HEADER_LENGTH; i < message_length - 1; i++) { + checksum ^= message[i]; + } + // Did our computed checksum and the provided checksum match? + if (checksum != message[message_length - 1]) { + return false; + } + // Check if this is a short swing/fix message + if (message[4] & TOSHIBA_COMMAND_MOTION) { + // Not supported yet + return false; + } + + // Get the mode + switch (message[6] & 0x0F) { + case TOSHIBA_MODE_OFF: + this->mode = climate::CLIMATE_MODE_OFF; + break; + + case TOSHIBA_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + + case TOSHIBA_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + + case TOSHIBA_MODE_FAN_ONLY: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + + case TOSHIBA_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + case TOSHIBA_MODE_AUTO: + default: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + } + + // Get the target temperature + this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN; + } + + this->publish_state(); + return true; +} + +void ToshibaClimate::encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, const uint8_t nbytes, + const uint8_t repeat) { + data->set_carrier_frequency(TOSHIBA_CARRIER_FREQUENCY); + + for (uint8_t copy = 0; copy <= repeat; copy++) { + data->item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE); + + for (uint8_t byte = 0; byte < nbytes; byte++) { for (uint8_t bit = 0; bit < 8; bit++) { data->mark(TOSHIBA_BIT_MARK); if (message[byte] & (1 << (7 - bit))) { @@ -118,87 +690,24 @@ void ToshibaClimate::transmit_state() { } } } - - data->mark(TOSHIBA_BIT_MARK); - data->space(TOSHIBA_GAP_SPACE); + data->item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE); } - - transmit.perform(); } -bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { - uint8_t message[16] = {0}; - uint8_t message_length = 4; - - /* Validate header */ - if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { - return false; - } - - /* Decode bytes */ - for (uint8_t byte = 0; byte < message_length; byte++) { +bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *message, const uint8_t nbytes) { + for (uint8_t byte = 0; byte < nbytes; byte++) { for (uint8_t bit = 0; bit < 8; bit++) { - if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { + if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { message[byte] |= 1 << (7 - bit); - } else if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { - /* Bit is already clear */ + } else if (data->expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { + message[byte] &= static_cast(~(1 << (7 - bit))); } else { return false; } } - - /* Update length */ - if (byte == 3) { - /* Validate the first checksum before trusting the length field */ - if ((message[0] ^ message[1] ^ message[2]) != message[3]) { - return false; - } - message_length = message[2] + 6; - } } - - /* Validate the second checksum before trusting any more of the message */ - uint8_t checksum = 0; - for (uint8_t i = 4; i < message_length - 1; i++) { - checksum ^= message[i]; - } - - if (checksum != message[message_length - 1]) { - return false; - } - - /* Check if this is a short swing/fix message */ - if (message[4] & TOSHIBA_COMMAND_MOTION) { - /* Not supported yet */ - return false; - } - - /* Get the mode. */ - switch (message[6] & 0x0f) { - case TOSHIBA_MODE_OFF: - this->mode = climate::CLIMATE_MODE_OFF; - break; - - case TOSHIBA_MODE_HEAT: - this->mode = climate::CLIMATE_MODE_HEAT; - break; - - case TOSHIBA_MODE_COOL: - this->mode = climate::CLIMATE_MODE_COOL; - break; - - case TOSHIBA_MODE_AUTO: - default: - /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */ - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - } - - /* Get the target temperature */ - this->target_temperature = (message[5] >> 4) + 17; - - this->publish_state(); return true; } -} /* namespace toshiba */ -} /* namespace esphome */ +} // namespace toshiba +} // namespace esphome diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index 3ab0dcdcdb..36e8760169 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -5,17 +5,69 @@ namespace esphome { namespace toshiba { -const float TOSHIBA_TEMP_MIN = 17.0; -const float TOSHIBA_TEMP_MAX = 30.0; +// Simple enum to represent models. +enum Model { + MODEL_GENERIC = 0, // Temperature range is from 17 to 30 + MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30 + MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30 +}; + +// Supported temperature ranges +const float TOSHIBA_GENERIC_TEMP_C_MIN = 17.0; +const float TOSHIBA_GENERIC_TEMP_C_MAX = 30.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN = 16.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0; +const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0; class ToshibaClimate : public climate_ir::ClimateIR { public: - ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_TEMP_MIN, TOSHIBA_TEMP_MAX, 1.0f) {} + ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f) {} + + void setup() override; + void set_model(Model model) { this->model_ = model; } protected: void transmit_state() override; + void transmit_generic_(); + void transmit_rac_pt1411hwru_(); + void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true); + // Returns the header if valid, else returns zero + uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message); + // Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet + bool is_valid_rac_pt1411hwru_message_(const uint8_t *message); + // Returns true if message1 and message 2 are the same + bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2); bool on_receive(remote_base::RemoteReceiveData data) override; + + float temperature_min_() { + return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MIN : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN; + } + float temperature_max_() { + return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX; + } + bool toshiba_supports_dry_() { + return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); + } + bool toshiba_supports_fan_only_() { + return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F)); + } + std::set toshiba_fan_modes_() { + return (this->model_ == MODEL_GENERIC) + ? std::set{} + : std::set{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}; + } + std::set toshiba_swing_modes_() { + return (this->model_ == MODEL_GENERIC) + ? std::set{} + : std::set{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; + } + void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); + bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); + + Model model_; }; -} /* namespace toshiba */ -} /* namespace esphome */ +} // namespace toshiba +} // namespace esphome diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 7fdd176d42..ec38daaf57 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -5,15 +5,14 @@ from esphome.const import ( CONF_ID, CONF_TIME_ID, DEVICE_CLASS_ENERGY, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) DEPENDENCIES = ["time"] CONF_POWER_ID = "power_id" +CONF_MIN_SAVE_INTERVAL = "min_save_interval" total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy") TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component @@ -21,18 +20,19 @@ TotalDailyEnergy = total_daily_energy_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ) .extend( { cv.GenerateID(): cv.declare_id(TotalDailyEnergy), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), + cv.Optional( + CONF_MIN_SAVE_INTERVAL, default="0s" + ): cv.positive_time_period_milliseconds, } ) .extend(cv.COMPONENT_SCHEMA) @@ -49,3 +49,4 @@ async def to_code(config): cg.add(var.set_parent(sens)) time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) + cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 8c5ef8c137..1e60442ae7 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -16,6 +16,7 @@ void TotalDailyEnergy::setup() { this->publish_state_and_save(0); } this->last_update_ = millis(); + this->last_save_ = this->last_update_; this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); } @@ -37,9 +38,14 @@ void TotalDailyEnergy::loop() { } } void TotalDailyEnergy::publish_state_and_save(float state) { - this->pref_.save(&state); this->total_energy_ = state; this->publish_state(state); + const uint32_t now = millis(); + if (now - this->last_save_ < this->min_save_interval_) { + return; + } + this->last_save_ = now; + this->pref_.save(&state); } void TotalDailyEnergy::process_new_state_(float state) { if (isnan(state)) diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index ae44125ffb..123446c534 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -10,6 +10,7 @@ namespace total_daily_energy { class TotalDailyEnergy : public sensor::Sensor, public Component { public: + void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; } void set_time(time::RealTimeClock *time) { time_ = time; } void set_parent(Sensor *parent) { parent_ = parent; } void setup() override; @@ -30,6 +31,8 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { Sensor *parent_; uint16_t last_day_of_year_{}; uint32_t last_update_{0}; + uint32_t last_save_{0}; + uint32_t min_save_interval_{0}; float total_energy_{0.0f}; }; diff --git a/esphome/components/tsl2561/sensor.py b/esphome/components/tsl2561/sensor.py index c05079f668..cf3837cb4d 100644 --- a/esphome/components/tsl2561/sensor.py +++ b/esphome/components/tsl2561/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_INTEGRATION_TIME, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_LUX, ) @@ -41,7 +40,10 @@ TSL2561Sensor = tsl2561_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tsl2591/__init__.py b/esphome/components/tsl2591/__init__.py new file mode 100644 index 0000000000..63331641c5 --- /dev/null +++ b/esphome/components/tsl2591/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@wjcarpenter"] diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py new file mode 100644 index 0000000000..095a8c886c --- /dev/null +++ b/esphome/components/tsl2591/sensor.py @@ -0,0 +1,168 @@ +# Credit where due.... +# I put a certain amount of work into this, but a lot of ESPHome integration is +# "look for other examples and see what they do" programming-by-example. Here are +# things that helped me along with this: +# +# - I mined the existing tsl2561 integration for basic structural framing for both +# the code and documentation. +# +# - I looked at the existing bme280 integration as an example of a single device +# with multiple sensors. +# +# - Comments and code in this thread got me going with the Adafruit TSL2591 library +# and prompted my desired to have tsl2591 as a standard component instead of a +# custom/external component. +# +# - And, of course, the handy and available Adafruit TSL2591 library was very +# helpful in understanding what the device is actually talking about. +# +# Here is the project that started me down the TSL2591 device trail in the first +# place: https://hackaday.io/project/176690-the-water-watcher + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_GAIN, + CONF_ID, + CONF_NAME, + CONF_INTEGRATION_TIME, + CONF_FULL_SPECTRUM, + CONF_INFRARED, + CONF_POWER_SAVE_MODE, + CONF_VISIBLE, + CONF_CALCULATED_LUX, + CONF_DEVICE_FACTOR, + CONF_GLASS_ATTENUATION_FACTOR, + ICON_BRIGHTNESS_6, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_EMPTY, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] + +tsl2591_ns = cg.esphome_ns.namespace("tsl2591") + +TSL2591IntegrationTime = tsl2591_ns.enum("TSL2591IntegrationTime") +INTEGRATION_TIMES = { + 100: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_100MS, + 200: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_200MS, + 300: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_300MS, + 400: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_400MS, + 500: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_500MS, + 600: TSL2591IntegrationTime.TSL2591_INTEGRATION_TIME_600MS, +} + +TSL2591Gain = tsl2591_ns.enum("TSL2591Gain") +GAINS = { + "1X": TSL2591Gain.TSL2591_GAIN_LOW, + "LOW": TSL2591Gain.TSL2591_GAIN_LOW, + "25X": TSL2591Gain.TSL2591_GAIN_MED, + "MED": TSL2591Gain.TSL2591_GAIN_MED, + "MEDIUM": TSL2591Gain.TSL2591_GAIN_MED, + "400X": TSL2591Gain.TSL2591_GAIN_HIGH, + "HIGH": TSL2591Gain.TSL2591_GAIN_HIGH, + "9500X": TSL2591Gain.TSL2591_GAIN_MAX, + "MAX": TSL2591Gain.TSL2591_GAIN_MAX, + "MAXIMUM": TSL2591Gain.TSL2591_GAIN_MAX, +} + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +TSL2591Component = tsl2591_ns.class_( + "TSL2591Component", cg.PollingComponent, i2c.I2CDevice +) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TSL2591Component), + cv.Optional(CONF_INFRARED): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VISIBLE): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FULL_SPECTRUM): sensor.sensor_schema( + UNIT_EMPTY, + ICON_BRIGHTNESS_6, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CALCULATED_LUX): sensor.sensor_schema( + UNIT_LUX, + ICON_BRIGHTNESS_6, + 4, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_NAME, default="TLS2591"): cv.string, + cv.Optional(CONF_GAIN, default="MEDIUM"): cv.enum(GAINS, upper=True), + cv.Optional(CONF_POWER_SAVE_MODE, default=True): cv.boolean, + cv.Optional(CONF_DEVICE_FACTOR, default=53.0): cv.float_with_unit( + "device_factor", "", True + ), + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=7.7): cv.float_with_unit( + "glass_attenuation_factor", "", True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x29)) +) + + +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_FULL_SPECTRUM in config: + conf = config[CONF_FULL_SPECTRUM] + sens = await sensor.new_sensor(conf) + cg.add(var.set_full_spectrum_sensor(sens)) + + if CONF_INFRARED in config: + conf = config[CONF_INFRARED] + sens = await sensor.new_sensor(conf) + cg.add(var.set_infrared_sensor(sens)) + + if CONF_VISIBLE in config: + conf = config[CONF_VISIBLE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_visible_sensor(sens)) + + if CONF_CALCULATED_LUX in config: + conf = config[CONF_CALCULATED_LUX] + sens = await sensor.new_sensor(conf) + cg.add(var.set_calculated_lux_sensor(sens)) + + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add( + var.set_device_and_glass_attenuation_factors( + config[CONF_DEVICE_FACTOR], config[CONF_GLASS_ATTENUATION_FACTOR] + ) + ) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp new file mode 100644 index 0000000000..1785fa46b4 --- /dev/null +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -0,0 +1,369 @@ +#include "tsl2591.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tsl2591 { + +static const char *const TAG = "tsl2591.sensor"; + +// Various constants used in TSL2591 register manipulation +#define TSL2591_COMMAND_BIT (0xA0) // 1010 0000: bits 7 and 5 for 'command, normal' +#define TSL2591_ENABLE_POWERON (0x01) // Flag for ENABLE register, to enable +#define TSL2591_ENABLE_POWEROFF (0x00) // Flag for ENABLE register, to disable +#define TSL2591_ENABLE_AEN (0x02) // Flag for ENABLE register, to turn on ADCs + +// TSL2591 registers from the datasheet. We only define what we use. +#define TSL2591_REGISTER_ENABLE (0x00) +#define TSL2591_REGISTER_CONTROL (0x01) +#define TSL2591_REGISTER_DEVICE_ID (0x12) +#define TSL2591_REGISTER_STATUS (0x13) +#define TSL2591_REGISTER_CHAN0_LOW (0x14) +#define TSL2591_REGISTER_CHAN0_HIGH (0x15) +#define TSL2591_REGISTER_CHAN1_LOW (0x16) +#define TSL2591_REGISTER_CHAN1_HIGH (0x17) + +void TSL2591Component::enable() { + // Enable the device by setting the control bit to 0x01. Also turn on ADCs. + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWERON | TSL2591_ENABLE_AEN)) { + ESP_LOGE(TAG, "Failed I2C write during enable()"); + } +} + +void TSL2591Component::disable() { + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_ENABLE, TSL2591_ENABLE_POWEROFF)) { + ESP_LOGE(TAG, "Failed I2C write during disable()"); + } +} + +void TSL2591Component::disable_if_power_saving_() { + if (this->power_save_mode_enabled_) { + this->disable(); + } +} + +void TSL2591Component::setup() { + uint8_t address = this->address_; + ESP_LOGI(TAG, "Setting up TSL2591 sensor at I2C address 0x%02X", address); + uint8_t id; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_DEVICE_ID, &id)) { + ESP_LOGE(TAG, "Failed I2C read during setup()"); + this->mark_failed(); + return; + } + if (id != 0x50) { + ESP_LOGE(TAG, + "Could not find the TSL2591 sensor. The ID register of the device at address 0x%02X reported 0x%02X " + "instead of 0x50.", + address, id); + this->mark_failed(); + return; + } + this->set_integration_time_and_gain(this->integration_time_, this->gain_); + this->disable_if_power_saving_(); +} + +void TSL2591Component::dump_config() { + ESP_LOGCONFIG(TAG, "TSL2591:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TSL2591 failed earlier, during setup"); + return; + } + + ESP_LOGCONFIG(TAG, " Name: %s", this->name_); + TSL2591Gain raw_gain = this->gain_; + int gain = 0; + std::string gain_word = "unknown"; + switch (raw_gain) { + case TSL2591_GAIN_LOW: + gain = 1; + gain_word = "low"; + break; + case TSL2591_GAIN_MED: + gain = 25; + gain_word = "medium"; + break; + case TSL2591_GAIN_HIGH: + gain = 400; + gain_word = "high"; + break; + case TSL2591_GAIN_MAX: + gain = 9500; + gain_word = "maximum"; + break; + } + ESP_LOGCONFIG(TAG, " Gain: %dx (%s)", gain, gain_word.c_str()); + TSL2591IntegrationTime raw_timing = this->integration_time_; + int timing_ms = (1 + raw_timing) * 100; + ESP_LOGCONFIG(TAG, " Integration Time: %d ms", timing_ms); + ESP_LOGCONFIG(TAG, " Power save mode enabled: %s", ONOFF(this->power_save_mode_enabled_)); + ESP_LOGCONFIG(TAG, " Device factor: %f", this->device_factor_); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + LOG_SENSOR(" ", "Full spectrum:", this->full_spectrum_sensor_); + LOG_SENSOR(" ", "Infrared:", this->infrared_sensor_); + LOG_SENSOR(" ", "Visible:", this->visible_sensor_); + LOG_SENSOR(" ", "Calculated lux:", this->calculated_lux_sensor_); + + LOG_UPDATE_INTERVAL(this); +} + +void TSL2591Component::process_update_() { + uint32_t combined = this->get_combined_illuminance(); + uint16_t visible = this->get_illuminance(TSL2591_SENSOR_CHANNEL_VISIBLE, combined); + uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined); + uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined); + float lux = this->get_calculated_lux(full, infrared); + ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f", combined, full, infrared, + visible, lux); + if (this->full_spectrum_sensor_ != nullptr) { + this->full_spectrum_sensor_->publish_state(full); + } + if (this->infrared_sensor_ != nullptr) { + this->infrared_sensor_->publish_state(infrared); + } + if (this->visible_sensor_ != nullptr) { + this->visible_sensor_->publish_state(visible); + } + if (this->calculated_lux_sensor_ != nullptr) { + this->calculated_lux_sensor_->publish_state(lux); + } + this->status_clear_warning(); +} + +#define interval_name "tsl2591_interval_for_update" + +void TSL2591Component::interval_function_for_update_() { + if (!this->is_adc_valid()) { + uint64_t now = millis(); + ESP_LOGD(TAG, "Elapsed %3llu ms; still waiting for valid ADC", (now - this->interval_start_)); + if (now > this->interval_timeout_) { + ESP_LOGW(TAG, "Interval timeout for TSL2591 '%s' expired before ADCs became valid.", this->name_); + this->cancel_interval(interval_name); + } + return; + } + this->cancel_interval(interval_name); + this->process_update_(); +} + +void TSL2591Component::update() { + if (!is_failed()) { + if (this->power_save_mode_enabled_) { + // we enabled it here, else ADC will never become valid + // but actually doing the reads will disable device if needed + this->enable(); + } + if (this->is_adc_valid()) { + this->process_update_(); + } else { + this->interval_start_ = millis(); + this->interval_timeout_ = this->interval_start_ + 620; + this->set_interval(interval_name, 100, [this] { this->interval_function_for_update_(); }); + } + } +} + +void TSL2591Component::set_infrared_sensor(sensor::Sensor *infrared_sensor) { + this->infrared_sensor_ = infrared_sensor; +} + +void TSL2591Component::set_visible_sensor(sensor::Sensor *visible_sensor) { this->visible_sensor_ = visible_sensor; } + +void TSL2591Component::set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor) { + this->full_spectrum_sensor_ = full_spectrum_sensor; +} + +void TSL2591Component::set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor) { + this->calculated_lux_sensor_ = calculated_lux_sensor; +} + +void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_time) { + this->integration_time_ = integration_time; +} + +void TSL2591Component::set_gain(TSL2591Gain gain) { this->gain_ = gain; } + +void TSL2591Component::set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor) { + this->device_factor_ = device_factor; + this->glass_attenuation_factor_ = glass_attenuation_factor; +} + +void TSL2591Component::set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain) { + this->enable(); + this->integration_time_ = integration_time; + this->gain_ = gain; + if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CONTROL, + this->integration_time_ | this->gain_)) { // NOLINT + ESP_LOGE(TAG, "Failed I2C write during set_integration_time_and_gain()"); + } + // The ADC values can be confused if gain or integration time are changed in the middle of a cycle. + // So, we unconditionally disable the device to turn the ADCs off. When re-enabling, the ADCs + // will tell us when they are ready again. That avoids an initial bogus reading. + this->disable(); + if (!this->power_save_mode_enabled_) { + this->enable(); + } +} + +void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_enabled_ = enable; } + +void TSL2591Component::set_name(const char *name) { this->name_ = name; } + +float TSL2591Component::get_setup_priority() const { return setup_priority::DATA; } + +bool TSL2591Component::is_adc_valid() { + uint8_t status; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { + ESP_LOGE(TAG, "Failed I2C read during is_adc_valid()"); + return false; + } + return status & 0x01; +} + +uint32_t TSL2591Component::get_combined_illuminance() { + this->enable(); + // Wait x ms for ADC to complete and signal valid. + // The max integration time is 600ms, so that's our max delay. + // (But we use 620ms as a bit of slack.) + // We'll do mini-delays and break out as soon as the ADC is good. + bool avalid; + const uint8_t mini_delay = 100; + for (uint16_t d = 0; d < 620; d += mini_delay) { + avalid = this->is_adc_valid(); + if (avalid) { + break; + } + // we only log this if we need any delay, since normally we don't + ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? "true" : "false"); + delay(mini_delay); + } + if (!avalid) { + // still not valid after a sutiable delay + // we don't mark the device as failed since it might come around in the future (probably not :-() + ESP_LOGE(TAG, "tsl2591 device '%s' did not return valid readings.", this->name_); + this->disable_if_power_saving_(); + return 0; + } + + // CHAN0 must be read before CHAN1 + // See: https://forums.adafruit.com/viewtopic.php?f=19&t=124176 + // Also, low byte must be read before high byte.. + // We read the registers in the order described in the datasheet. + uint32_t x32; + uint8_t ch0low, ch0high, ch1low, ch1high; + uint16_t ch0_16; + uint16_t ch1_16; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_LOW, &ch0low)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN0_HIGH, &ch0high)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + ch0_16 = (ch0high << 8) | ch0low; + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_LOW, &ch1low)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CHAN1_HIGH, &ch1high)) { + ESP_LOGE(TAG, "Failed I2C read during get_combined_illuminance()"); + return 0; + } + ch1_16 = (ch1high << 8) | ch1low; + x32 = (ch1_16 << 16) | ch0_16; + + this->disable_if_power_saving_(); + return x32; +} + +uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel) { + uint32_t combined = this->get_combined_illuminance(); + return this->get_illuminance(channel, combined); +} +// logic cloned from Adafruit TSL2591 library +uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance) { + if (channel == TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM) { + // Reads two byte value from channel 0 (visible + infrared) + return (combined_illuminance & 0xFFFF); + } else if (channel == TSL2591_SENSOR_CHANNEL_INFRARED) { + // Reads two byte value from channel 1 (infrared) + return (combined_illuminance >> 16); + } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { + // Reads all and subtracts out the infrared + return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); + } + // unknown channel! + ESP_LOGE(TAG, "TSL2591Component::get_illuminance() caller requested an unknown channel: %d", channel); + return 0; +} + +/** Calculates a lux value from the two TSL2591 physical sensor ADC readings. + * + * The lux calculation is copied from the Adafruit TSL2591 library. + * There is some debate about whether it is the correct lux equation to use. + * We use that lux equation because (a) it helps with a transition from + * using that Adafruit library to using this ESPHome integration, and (b) we + * don't have a definitive better idea. + * + * Since the raw ADC readings are available, you can ignore this method and + * implement your own lux equation. + * + * @param full_spectrum The ADC reading for TSL2591 channel 0. + * @param infrared The ADC reading for TSL2591 channel 1. + */ +float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infrared) { + // Check for overflow conditions first + uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535); + if ((full_spectrum == max_count) || (infrared == max_count)) { + // Signal an overflow + ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain.", this->name_); + return -1.0F; + } + + if ((full_spectrum == 0) && (infrared == 0)) { + // trivial conversion; avoids divide by 0 + ESP_LOGW(TAG, "Zero reading on both TSL2591 (%s) sensors. Is the device having a problem?", this->name_); + return 0.0F; + } + + float atime = 100.F + (this->integration_time_ * 100); + + float again; + switch (this->gain_) { + case TSL2591_GAIN_LOW: + again = 1.0F; + break; + case TSL2591_GAIN_MED: + again = 25.0F; + break; + case TSL2591_GAIN_HIGH: + again = 400.0F; + break; + case TSL2591_GAIN_MAX: + again = 9500.0F; + break; + default: + again = 1.0F; + break; + } + + // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly. + // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14 + // and that library code. + // They said: + // Note: This algorithm is based on preliminary coefficients + // provided by AMS and may need to be updated in the future + // However, we use gain multipliers that are more in line with the midpoints + // of ranges from the datasheet. We don't know why the other libraries + // used the values they did for HIGH and MAX. + // If cpl or full_spectrum are 0, this will return NaN due to divide by 0. + // For the curious "cpl" is counts per lux, a term used in AMS application notes. + float cpl = (atime * again) / (this->device_factor_ * this->glass_attenuation_factor_); + float lux = (((float) full_spectrum - (float) infrared)) * (1.0F - ((float) infrared / (float) full_spectrum)) / cpl; + return max(lux, 0.0F); +} + +} // namespace tsl2591 +} // namespace esphome diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h new file mode 100644 index 0000000000..d377d082a8 --- /dev/null +++ b/esphome/components/tsl2591/tsl2591.h @@ -0,0 +1,245 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tsl2591 { + +/** Enum listing all conversion/integration time settings for the TSL2591. + * + * Specific values of the enum constants are register values taken from the TSL2591 datasheet. + * Longer times mean more accurate results, but will take more energy/more time. + */ +enum TSL2591IntegrationTime { + TSL2591_INTEGRATION_TIME_100MS = 0b000, + TSL2591_INTEGRATION_TIME_200MS = 0b001, + TSL2591_INTEGRATION_TIME_300MS = 0b010, + TSL2591_INTEGRATION_TIME_400MS = 0b011, + TSL2591_INTEGRATION_TIME_500MS = 0b100, + TSL2591_INTEGRATION_TIME_600MS = 0b101, +}; + +/** Enum listing all gain settings for the TSL2591. + * + * Specific values of the enum constants are register values taken from the TSL2591 datasheet. + * Higher values are better for low light situations, but can increase noise. + */ +enum TSL2591Gain { + TSL2591_GAIN_LOW = 0b00 << 4, // 1x + TSL2591_GAIN_MED = 0b01 << 4, // 25x + TSL2591_GAIN_HIGH = 0b10 << 4, // 400x + TSL2591_GAIN_MAX = 0b11 << 4, // 9500x +}; + +/** Enum listing sensor channels. + * + * They identify the type of light to report. + */ +enum TSL2591SensorChannel { + TSL2591_SENSOR_CHANNEL_VISIBLE, + TSL2591_SENSOR_CHANNEL_INFRARED, + TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, +}; + +/// This class includes support for the TSL2591 i2c ambient light +/// sensor. The device has two distinct sensors. One is for visible +/// light plus infrared light, and the other is for infrared +/// light. They are reported as separate sensors, and the difference +/// between the values is reported as a third sensor as a convenience +/// for visible light only. +class TSL2591Component : public PollingComponent, public i2c::I2CDevice { + public: + /** Set device integration time and gain. + * + * These are set as a single I2C transaction, so you must supply values + * for both. + * + * Longer integration times provides more accurate values, but also + * means more power consumption. Higher gain values are useful for + * lower light intensities but are also subject to more noise. The + * device might use a slightly different gain multiplier than those + * indicated; see the datasheet for details. + * + * Possible values for integration_time (from enum + * TSL2591IntegrationTime) are: + * + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_100MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_200MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_300MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_400MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_500MS` + * - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_600MS` + * + * Possible values for gain (from enum TSL2591Gain) are: + * + * - `esphome::tsl2591::TSL2591_GAIN_LOW` (1x) + * - `esphome::tsl2591::TSL2591_GAIN_MED` (25x) + * - `esphome::tsl2591::TSL2591_GAIN_HIGH` (400x) + * - `esphome::tsl2591::TSL2591_GAIN_MAX` (9500x) + * + * @param integration_time The new integration time. + * @param gain The new gain. + */ + void set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain); + + /** Should the device be powered down between readings? + * + * The disadvantage of powering down the device between readings + * is that you have to wait for the ADC to go through an + * integration cycle before a reliable reading is available. + * This happens during ESPHome's update loop, so waiting slows + * down the entire ESP device. You should only enable this if + * you need to minimize power consumption and you can tolerate + * that delay. Otherwise, keep the default of disabling + * power save mode. + * + * @param enable Enable or disable power save mode. + */ + void set_power_save_mode(bool enable); + + /** Sets the name for this instance of the device. + * + * @param name The user-friendly name. + */ + void set_name(const char *name); + + /** Sets the device and glass attenuation factors. + * + * The lux equation, used to calculate the lux from the ADC readings, + * involves a scaling coefficient that is the product of a device + * factor (specific to the type of device being used) and a glass + * attenuation factor (specific to whatever glass or plastic cover + * is installed in front of the light sensors. + * + * AMS does not publish the device factor for the TSL2591. In the + * datasheet for the earlier TSL2571 and in application notes, they + * use the value 53, so we use that as the default. + * + * The glass attenuation factor depends on factors external to the + * TSL2591 and is best obtained through experimental measurements. + * The Adafruit TSL2591 library use a value of ~7.7, which we use as + * a default. Waveshare uses a value of ~14.4. Presumably, those + * factors are appropriate to the breakout boards from those vendors, + * but we have not verified that. + * + * @param device_factor The device factor. + * @param glass_attenuation_factor The glass attenuation factor. + */ + void set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor); + + /** Calculates and returns a lux value based on the ADC readings. + * + * @param full_spectrum The ADC reading for the full spectrum sensor. + * @param infrared The ADC reading for the infrared sensor. + */ + float get_calculated_lux(uint16_t full_spectrum, uint16_t infrared); + + /** Get the combined illuminance value. + * + * This is encoded into a 32 bit value. The high 16 bits are the value of the + * infrared sensor. The low 16 bits are the sum of the combined sensor values. + * + * If power saving mode is enabled, there can be a delay (up to the value of the integration + * time) while waiting for the device ADCs to signal that values are valid. + */ + uint32_t get_combined_illuminance(); + + /** Get an individual sensor channel reading. + * + * This gets an individual light sensor reading. Since it goes through + * the entire component read cycle to get one value, it's not optimal if + * you want to get all possible channel values. If you want that, first + * call `get_combined_illuminance()` and pass that value to the companion + * method with a different signature. + * + * If power saving mode is enabled, there can be a delay (up to the value of the integration + * time) while waiting for the device ADCs to signal that values are valid. + * + * @param channel The sensor channel of interest. + */ + uint16_t get_illuminance(TSL2591SensorChannel channel); + + /** Get an individual sensor channel reading from combined illuminance. + * + * This gets an individual light sensor reading from a combined illuminance + * value, which you would obtain from calling `getCombinedIlluminance()`. + * This method does not communicate with the sensor at all. It's strictly + * local calculations, so it is efficient if you call it multiple times. + * + * @param channel The sensor channel of interest. + * @param combined_illuminance The previously obtained combined illuminance value. + */ + uint16_t get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance); + + /** Are the device ADC values valid? + * + * Useful for scripting. This should be checked before calling update(). + * It asks the TSL2591 if the ADC has completed an integration cycle + * and has reliable values in the device registers. If you call update() + * before the ADC values are valid, you may cause a general delay in + * the ESPHome update loop. + * + * It should take no more than the configured integration time for + * the ADC values to become valid after the TSL2591 device is enabled. + */ + bool is_adc_valid(); + + /** Powers on the TSL2591 device and enables its sensors. + * + * You only need to call this if you have disabled the device. + * The device starts enabled in ESPHome unless power save mode is enabled. + */ + void enable(); + /** Powers off the TSL2591 device. + * + * You can call this from an ESPHome script if you are explicitly + * controlling TSL2591 power consumption. + * The device starts enabled in ESPHome unless power save mode is enabled. + */ + void disable(); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these. They're for ESPHome integration use.) + /** Used by ESPHome framework. */ + void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor); + /** Used by ESPHome framework. */ + void set_infrared_sensor(sensor::Sensor *infrared_sensor); + /** Used by ESPHome framework. */ + void set_visible_sensor(sensor::Sensor *visible_sensor); + /** Used by ESPHome framework. */ + void set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor); + /** Used by ESPHome framework. Does NOT actually set the value on the device. */ + void set_integration_time(TSL2591IntegrationTime integration_time); + /** Used by ESPHome framework. Does NOT actually set the value on the device. */ + void set_gain(TSL2591Gain gain); + /** Used by ESPHome framework. */ + void setup() override; + /** Used by ESPHome framework. */ + void dump_config() override; + /** Used by ESPHome framework. */ + void update() override; + /** Used by ESPHome framework. */ + float get_setup_priority() const override; + + protected: + const char *name_; // TODO: extend esphome::Nameable + sensor::Sensor *full_spectrum_sensor_; + sensor::Sensor *infrared_sensor_; + sensor::Sensor *visible_sensor_; + sensor::Sensor *calculated_lux_sensor_; + TSL2591IntegrationTime integration_time_; + TSL2591Gain gain_; + bool power_save_mode_enabled_; + float device_factor_; + float glass_attenuation_factor_; + uint64_t interval_start_; + uint64_t interval_timeout_; + void disable_if_power_saving_(); + void process_update_(); + void interval_function_for_update_(); +}; + +} // namespace tsl2591 +} // namespace esphome diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 5f214d3bfe..a4ce06476d 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -54,14 +54,14 @@ void TuyaClimate::control(const climate::ClimateCall &call) { if (call.get_mode().has_value()) { const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); - this->parent_->set_datapoint_value(*this->switch_id_, switch_state); + this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); } if (call.get_target_temperature().has_value()) { const float target_temperature = *call.get_target_temperature(); ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature); - this->parent_->set_datapoint_value(*this->target_temperature_id_, - (int) (target_temperature / this->target_temperature_multiplier_)); + this->parent_->set_integer_datapoint_value(*this->target_temperature_id_, + (int) (target_temperature / this->target_temperature_multiplier_)); } } diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index ed92713e52..8738b7f4a0 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -67,20 +67,20 @@ void TuyaFan::dump_config() { void TuyaFan::write_state() { if (this->switch_id_.has_value()) { ESP_LOGV(TAG, "Setting switch: %s", ONOFF(this->fan_->state)); - this->parent_->set_datapoint_value(*this->switch_id_, this->fan_->state); + this->parent_->set_boolean_datapoint_value(*this->switch_id_, this->fan_->state); } if (this->oscillation_id_.has_value()) { ESP_LOGV(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating)); - this->parent_->set_datapoint_value(*this->oscillation_id_, this->fan_->oscillating); + this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, this->fan_->oscillating); } if (this->direction_id_.has_value()) { bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; ESP_LOGV(TAG, "Setting reverse direction: %s", ONOFF(enable)); - this->parent_->set_datapoint_value(*this->direction_id_, enable); + this->parent_->set_boolean_datapoint_value(*this->direction_id_, enable); } if (this->speed_id_.has_value()) { ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed); - this->parent_->set_datapoint_value(*this->speed_id_, this->fan_->speed - 1); + this->parent_->set_integer_datapoint_value(*this->speed_id_, this->fan_->speed - 1); } } diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index d7e3561328..8ad78aacea 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -31,7 +31,7 @@ void TuyaLight::setup() { }); } if (min_value_datapoint_id_.has_value()) { - parent_->set_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); + parent_->set_integer_datapoint_value(*this->min_value_datapoint_id_, this->min_value_); } } @@ -45,11 +45,14 @@ void TuyaLight::dump_config() { light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); - traits.set_supports_brightness(this->dimmer_id_.has_value()); - traits.set_supports_color_temperature(this->color_temperature_id_.has_value()); - if (this->color_temperature_id_.has_value()) { + if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { + traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); + } else if (this->dimmer_id_.has_value()) { + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + } else { + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); } return traits; } @@ -63,9 +66,9 @@ void TuyaLight::write_state(light::LightState *state) { if (brightness == 0.0f) { // turning off, first try via switch (if exists), then dimmer if (switch_id_.has_value()) { - parent_->set_datapoint_value(*this->switch_id_, false); + parent_->set_boolean_datapoint_value(*this->switch_id_, false); } else if (dimmer_id_.has_value()) { - parent_->set_datapoint_value(*this->dimmer_id_, 0); + parent_->set_integer_datapoint_value(*this->dimmer_id_, 0); } return; } @@ -75,17 +78,17 @@ void TuyaLight::write_state(light::LightState *state) { static_cast(this->color_temperature_max_value_ * (state->current_values.get_color_temperature() - this->cold_white_temperature_) / (this->warm_white_temperature_ - this->cold_white_temperature_)); - parent_->set_datapoint_value(*this->color_temperature_id_, color_temp_int); + parent_->set_integer_datapoint_value(*this->color_temperature_id_, color_temp_int); } auto brightness_int = static_cast(brightness * this->max_value_); brightness_int = std::max(brightness_int, this->min_value_); if (this->dimmer_id_.has_value()) { - parent_->set_datapoint_value(*this->dimmer_id_, brightness_int); + parent_->set_integer_datapoint_value(*this->dimmer_id_, brightness_int); } if (this->switch_id_.has_value()) { - parent_->set_datapoint_value(*this->switch_id_, true); + parent_->set_boolean_datapoint_value(*this->switch_id_, true); } } diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp index 8cd09fb01c..cbd794b001 100644 --- a/esphome/components/tuya/switch/tuya_switch.cpp +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -15,7 +15,7 @@ void TuyaSwitch::setup() { void TuyaSwitch::write_state(bool state) { ESP_LOGV(TAG, "Setting switch %u: %s", this->switch_id_, ONOFF(state)); - this->parent_->set_datapoint_value(this->switch_id_, state); + this->parent_->set_boolean_datapoint_value(this->switch_id_, state); this->publish_state(state); } diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 325f12a58d..e42c74005e 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -165,7 +165,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->gpio_reset_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { - // If mcu returned status gpio, then we can ommit sending wifi state + // If mcu returned status gpio, then we can omit sending wifi state if (this->gpio_status_ != -1) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); @@ -297,7 +297,7 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask); break; default: - ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, datapoint.type); + ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast(datapoint.type)); return; } @@ -370,7 +370,7 @@ void Tuya::process_command_queue_() { this->expected_response_.reset(); } - // Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly + // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); @@ -437,42 +437,38 @@ void Tuya::send_local_time_() { } #endif -void Tuya::set_datapoint_value(uint8_t datapoint_id, uint32_t value) { - ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value); +void Tuya::set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value) { + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, hexencode(value).c_str()); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { - ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id); + ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); + } else if (datapoint->type != TuyaDatapointType::RAW) { + ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); return; - } - if (datapoint->value_uint == value) { + } else if (datapoint->value_raw == value) { ESP_LOGV(TAG, "Not sending unchanged value"); return; } - - std::vector data; - switch (datapoint->len) { - case 4: - data.push_back(value >> 24); - data.push_back(value >> 16); - case 2: - data.push_back(value >> 8); - case 1: - data.push_back(value >> 0); - break; - default: - ESP_LOGE(TAG, "Unexpected datapoint length %zu", datapoint->len); - return; - } - this->send_datapoint_command_(datapoint->id, datapoint->type, data); + this->send_datapoint_command_(datapoint_id, TuyaDatapointType::RAW, value); } -void Tuya::set_datapoint_value(uint8_t datapoint_id, const std::string &value) { +void Tuya::set_boolean_datapoint_value(uint8_t datapoint_id, bool value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1); +} + +void Tuya::set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4); +} + +void Tuya::set_string_datapoint_value(uint8_t datapoint_id, const std::string &value) { ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str()); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { - ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id); - } - if (datapoint->value_string == value) { + ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); + } else if (datapoint->type != TuyaDatapointType::STRING) { + ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); + return; + } else if (datapoint->value_string == value) { ESP_LOGV(TAG, "Not sending unchanged value"); return; } @@ -483,6 +479,14 @@ void Tuya::set_datapoint_value(uint8_t datapoint_id, const std::string &value) { this->send_datapoint_command_(datapoint->id, datapoint->type, data); } +void Tuya::set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1); +} + +void Tuya::set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length) { + this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BITMASK, value, length); +} + optional Tuya::get_datapoint_(uint8_t datapoint_id) { for (auto &datapoint : this->datapoints_) if (datapoint.id == datapoint_id) @@ -490,6 +494,37 @@ optional Tuya::get_datapoint_(uint8_t datapoint_id) { return {}; } +void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value, + uint8_t length) { + ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value); + optional datapoint = this->get_datapoint_(datapoint_id); + if (!datapoint.has_value()) { + ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); + } else if (datapoint->type != datapoint_type) { + ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id); + return; + } else if (datapoint->value_uint == value) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + + std::vector data; + switch (length) { + case 4: + data.push_back(value >> 24); + data.push_back(value >> 16); + case 2: + data.push_back(value >> 8); + case 1: + data.push_back(value >> 0); + break; + default: + ESP_LOGE(TAG, "Unexpected datapoint length %u", length); + return; + } + this->send_datapoint_command_(datapoint_id, datapoint_type, data); +} + void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data) { std::vector buffer; buffer.push_back(datapoint_id); diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 7ce4be4315..785399502b 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -17,7 +17,7 @@ enum class TuyaDatapointType : uint8_t { INTEGER = 0x02, // 4 byte STRING = 0x03, // variable length ENUM = 0x04, // 1 byte - BITMASK = 0x05, // 2 bytes + BITMASK = 0x05, // 1/2/4 bytes }; struct TuyaDatapoint { @@ -75,8 +75,12 @@ class Tuya : public Component, public uart::UARTDevice { void loop() override; void dump_config() override; void register_listener(uint8_t datapoint_id, const std::function &func); - void set_datapoint_value(uint8_t datapoint_id, uint32_t value); - void set_datapoint_value(uint8_t datapoint_id, const std::string &value); + void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); + void set_boolean_datapoint_value(uint8_t datapoint_id, bool value); + void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); + void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value); + void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value); + void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length); #ifdef USE_TIME void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } #endif @@ -95,6 +99,8 @@ class Tuya : public Component, public uart::UARTDevice { void process_command_queue_(); void send_command_(const TuyaCommand &command); void send_empty_command_(TuyaCommandType command); + void set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, uint32_t value, + uint8_t length); void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); void send_wifi_status_(); diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 57c3165d16..ceb9b88d8d 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_WIND_SPEED, CONF_PIN, CONF_WIND_DIRECTION_DEGREES, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_KILOMETER_PER_HOUR, @@ -23,14 +22,16 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Tx20Component), cv.Optional(CONF_WIND_SPEED): sensor.sensor_schema( - UNIT_KILOMETER_PER_HOUR, - ICON_WEATHER_WINDY, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_WIND_DIRECTION_DEGREES): sensor.sensor_schema( - UNIT_DEGREES, ICON_SIGN_DIRECTION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SIGN_DIRECTION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Required(CONF_PIN): cv.All( pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index cd54290c73..1dc1e18412 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/core/esphal.h" #include "esphome/core/component.h" diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp index 89de4c0cc1..16d683e4a6 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_esp32.cpp @@ -12,7 +12,7 @@ uint8_t next_uart_num = 1; static const uint32_t UART_PARITY_EVEN = 0 << 0; static const uint32_t UART_PARITY_ODD = 1 << 0; -static const uint32_t UART_PARITY_EN = 1 << 1; +static const uint32_t UART_PARITY_ENABLE = 1 << 1; static const uint32_t UART_NB_BIT_5 = 0 << 2; static const uint32_t UART_NB_BIT_6 = 1 << 2; static const uint32_t UART_NB_BIT_7 = 2 << 2; @@ -39,9 +39,9 @@ uint32_t UARTComponent::get_config() { */ if (this->parity_ == UART_CONFIG_PARITY_EVEN) - config |= UART_PARITY_EVEN | UART_PARITY_EN; + config |= UART_PARITY_EVEN | UART_PARITY_ENABLE; else if (this->parity_ == UART_CONFIG_PARITY_ODD) - config |= UART_PARITY_ODD | UART_PARITY_EN; + config |= UART_PARITY_ODD | UART_PARITY_ENABLE; switch (this->data_bits_) { case 5: diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index 6f7d4c1f8b..c45f48644c 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -242,9 +242,9 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { bool parity_bit = false; bool need_parity_bit = true; if (this->parity_ == UART_CONFIG_PARITY_EVEN) - parity_bit = true; - else if (this->parity_ == UART_CONFIG_PARITY_ODD) parity_bit = false; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + parity_bit = true; else need_parity_bit = false; diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index d5f2cef05f..f7026e884c 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_TRIGGER_PIN, CONF_TIMEOUT, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -22,11 +21,10 @@ UltrasonicSensorComponent = ultrasonic_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index eaaee5a2d5..6ea3cca189 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_SECOND, ICON_TIMER, @@ -14,7 +13,10 @@ UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingCompone CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_SECOND, ICON_TIMER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt index fe33583414..f7a234d023 100644 --- a/esphome/components/vl53l0x/LICENSE.txt +++ b/esphome/components/vl53l0x/LICENSE.txt @@ -3,7 +3,7 @@ by Pololu (Pololu Corporation), which in turn is based on the VL53L0X API from ST. The code has been adapted to work with ESPHome's i2c APIs. Please see the top-level LICENSE.txt for information about ESPHome's license. The licenses for Pololu's and ST's software are included below. -Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). +Originally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). ================================================================= diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 8a9667a1bd..0ce3197366 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -42,11 +41,10 @@ def check_timeout(value): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 3e1132bb1d..e825456c36 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -31,6 +31,9 @@ WaveshareEPaper2P9InB = waveshare_epaper_ns.class_( WaveshareEPaper4P2In = waveshare_epaper_ns.class_( "WaveshareEPaper4P2In", WaveshareEPaper ) +WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper4P2InBV2", WaveshareEPaper +) WaveshareEPaper5P8In = waveshare_epaper_ns.class_( "WaveshareEPaper5P8In", WaveshareEPaper ) @@ -40,6 +43,9 @@ WaveshareEPaper7P5In = waveshare_epaper_ns.class_( WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2", WaveshareEPaper ) +WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( + "WaveshareEPaper2P13InDKE", WaveshareEPaper +) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel") @@ -51,21 +57,24 @@ MODELS = { "2.13in-ttgo": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), "2.13in-ttgo-b1": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B1), "2.13in-ttgo-b73": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B73), + "2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74), "2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), "2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2), "2.70in": ("b", WaveshareEPaper2P7In), "2.90in-b": ("b", WaveshareEPaper2P9InB), "4.20in": ("b", WaveshareEPaper4P2In), + "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), "7.50in": ("b", WaveshareEPaper7P5In), "7.50inv2": ("b", WaveshareEPaper7P5InV2), + "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), } def validate_full_update_every_only_type_a(value): if CONF_FULL_UPDATE_EVERY not in value: return value - if MODELS[value[CONF_MODEL]][0] != "a": + if MODELS[value[CONF_MODEL]][0] == "b": raise cv.Invalid( "The 'full_update_every' option is only available for models " "'1.54in', '1.54inV2', '2.13in', '2.90in', and '2.90inV2'." @@ -96,7 +105,7 @@ async def to_code(config): if model_type == "a": rhs = WaveshareEPaperTypeA.new(model) var = cg.Pvariable(config[CONF_ID], rhs, WaveshareEPaperTypeA) - elif model_type == "b": + elif model_type in ("b", "c"): rhs = model.new() var = cg.Pvariable(config[CONF_ID], rhs, model) else: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 4518dd60df..c32e7d27a0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -163,6 +163,17 @@ void WaveshareEPaper::on_safe_shutdown() { this->deep_sleep(); } // ======================================================== void WaveshareEPaperTypeA::initialize() { + if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + this->wait_until_idle_(); + + this->command(0x12); // SWRESET + this->wait_until_idle_(); + } + // COMMAND DRIVER OUTPUT CONTROL this->command(0x01); this->data(this->get_height_internal() - 1); @@ -193,6 +204,7 @@ void WaveshareEPaperTypeA::initialize() { case TTGO_EPAPER_2_13_IN_B1: this->data(0x01); // x increase, y decrease : as in demo code break; + case TTGO_EPAPER_2_13_IN_B74: case WAVESHARE_EPAPER_2_9_IN_V2: this->data(0x03); // from top left to bottom right // RAM content option for Display Update @@ -222,6 +234,9 @@ void WaveshareEPaperTypeA::dump_config() { case TTGO_EPAPER_2_13_IN_B73: ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B73)"); break; + case TTGO_EPAPER_2_13_IN_B74: + ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B74)"); + break; case TTGO_EPAPER_2_13_IN_B1: ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B1)"); break; @@ -256,6 +271,9 @@ void HOT WaveshareEPaperTypeA::display() { case TTGO_EPAPER_2_13_IN_B73: this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO_B73 : PARTIAL_UPDATE_LUT_TTGO_B73, LUT_SIZE_TTGO_B73); break; + case TTGO_EPAPER_2_13_IN_B74: + // there is no LUT + break; case TTGO_EPAPER_2_13_IN_B1: this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO_B1 : PARTIAL_UPDATE_LUT_TTGO_B1, LUT_SIZE_TTGO_B1); break; @@ -289,7 +307,12 @@ void HOT WaveshareEPaperTypeA::display() { this->data((this->get_height_internal() - 1) >> 8); break; + case TTGO_EPAPER_2_13_IN_B74: + // BorderWaveform + this->command(0x3C); + this->data(full_update ? 0x05 : 0x80); + // fall through default: // COMMAND SET RAM X ADDRESS START END POSITION this->command(0x44); @@ -341,6 +364,9 @@ void HOT WaveshareEPaperTypeA::display() { this->data(full_update ? 0xF7 : 0xFF); } else if (this->model_ == TTGO_EPAPER_2_13_IN_B73) { this->data(0xC7); + } else if (this->model_ == TTGO_EPAPER_2_13_IN_B74) { + // this->data(0xC7); + this->data(full_update ? 0xF7 : 0xFF); } else { this->data(0xC4); } @@ -363,6 +389,7 @@ int WaveshareEPaperTypeA::get_width_internal() { case TTGO_EPAPER_2_13_IN: return 128; case TTGO_EPAPER_2_13_IN_B73: + case TTGO_EPAPER_2_13_IN_B74: return 128; case TTGO_EPAPER_2_13_IN_B1: return 128; @@ -384,6 +411,7 @@ int WaveshareEPaperTypeA::get_height_internal() { case TTGO_EPAPER_2_13_IN: return 250; case TTGO_EPAPER_2_13_IN_B73: + case TTGO_EPAPER_2_13_IN_B74: return 250; case TTGO_EPAPER_2_13_IN_B1: return 250; @@ -598,7 +626,7 @@ void WaveshareEPaper2P9InB::initialize() { this->data(0x9F); // COMMAND RESOLUTION SETTING - // set to 128x296 by COMMAND PANNEL SETTING + // set to 128x296 by COMMAND PANEL SETTING // COMMAND VCOM AND DATA INTERVAL SETTING // use defaults for white border and ESPHome image polarity @@ -765,6 +793,62 @@ void WaveshareEPaper4P2In::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 4.20in Type B (LUT from OTP) +// Datasheet: +// - https://www.waveshare.com/w/upload/2/20/4.2inch-e-paper-module-user-manual-en.pdf +// - https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_4in2b_V2.c +// ======================================================== +void WaveshareEPaper4P2InBV2::initialize() { + // these exact timings are required for a proper reset/init + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x0f); // LUT from OTP +} + +void HOT WaveshareEPaper4P2InBV2::display() { + // COMMAND DATA START TRANSMISSION 1 (B/W data) + this->command(0x10); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DATA START TRANSMISSION 2 (RED data) + this->command(0x13); + this->start_data_(); + for (int i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0xFF); + this->end_data_(); + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper4P2InBV2::get_width_internal() { return 400; } +int WaveshareEPaper4P2InBV2::get_height_internal() { return 300; } +void WaveshareEPaper4P2InBV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 4.2in (B V2)"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + void WaveshareEPaper5P8In::initialize() { // COMMAND POWER SETTING this->command(0x01); @@ -996,5 +1080,136 @@ void WaveshareEPaper7P5InV2::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } + +static const uint8_t LUT_SIZE_TTGO_DKE_PART = 153; + +static const uint8_t PART_UPDATE_LUT_TTGO_DKE[LUT_SIZE_TTGO_DKE_PART] = { + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + // 0x22, 0x17, 0x41, 0x0, 0x32, 0x32 +}; + +void WaveshareEPaper2P13InDKE::initialize() {} +void HOT WaveshareEPaper2P13InDKE::display() { + bool partial = this->at_update_ != 0; + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + + if (partial) + ESP_LOGI(TAG, "Performing partial e-paper update."); + else + ESP_LOGI(TAG, "Performing full e-paper update."); + + // start and set up data format + this->command(0x12); + this->wait_until_idle_(); + + this->command(0x11); + this->data(0x03); + this->command(0x44); + this->data(1); + this->data(this->get_width_internal() / 8); + this->command(0x45); + this->data(0); + this->data(0); + this->data(this->get_height_internal()); + this->data(0); + this->command(0x4e); + this->data(1); + this->command(0x4f); + this->data(0); + this->data(0); + + if (!partial) { + // send data + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // commit + this->command(0x20); + this->wait_until_idle_(); + } else { + // set up partial update + this->command(0x32); + for (uint8_t v : PART_UPDATE_LUT_TTGO_DKE) + this->data(v); + this->command(0x3F); + this->data(0x22); + + this->command(0x03); + this->data(0x17); + this->command(0x04); + this->data(0x41); + this->data(0x00); + this->data(0x32); + this->command(0x2C); + this->data(0x32); + + this->command(0x37); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x40); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + + this->command(0x3C); + this->data(0x80); + this->command(0x22); + this->data(0xC0); + this->command(0x20); + this->wait_until_idle_(); + + // send data + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // commit as partial + this->command(0x22); + this->data(0xCF); + this->command(0x20); + this->wait_until_idle_(); + + // data must be sent again on partial update + delay(300); // NOLINT + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + delay(300); // NOLINT + } + + ESP_LOGI(TAG, "Completed e-paper update."); +} + +int WaveshareEPaper2P13InDKE::get_width_internal() { return 128; } +int WaveshareEPaper2P13InDKE::get_height_internal() { return 250; } +int WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } +void WaveshareEPaper2P13InDKE::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void WaveshareEPaper2P13InDKE::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 8ab77d653b..f7603c5af0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -73,6 +73,7 @@ enum WaveshareEPaperTypeAModel { TTGO_EPAPER_2_13_IN, TTGO_EPAPER_2_13_IN_B73, TTGO_EPAPER_2_13_IN_B1, + TTGO_EPAPER_2_13_IN_B74, }; class WaveshareEPaperTypeA : public WaveshareEPaper { @@ -115,6 +116,7 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, WAVESHARE_EPAPER_4_2_IN, + WAVESHARE_EPAPER_4_2_IN_B_V2, WAVESHARE_EPAPER_7_5_IN, WAVESHARE_EPAPER_7_5_INV2, }; @@ -202,6 +204,34 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper4P2InBV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0xF7); // border floating + + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check code + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper5P8In : public WaveshareEPaper { public: void initialize() override; @@ -271,5 +301,33 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P13InDKE : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER DOWN + this->command(0x10); + this->data(0x01); + // cannot wait until idle here, the device no longer responds + } + + void set_full_update_every(uint32_t full_update_every); + + protected: + int get_width_internal() override; + + int get_height_internal() override; + + int idle_timeout_() override; + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; +}; + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b775d44211..9dad61bb5b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -8,6 +8,10 @@ #include +#ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" +#endif + #ifdef USE_LOGGER #include #endif @@ -125,6 +129,12 @@ void WebServer::setup() { if (!obj->is_internal()) client->send(this->number_json(obj, obj->state).c_str(), "state"); #endif + +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + if (!obj->is_internal()) + client->send(this->select_json(obj, obj->state).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -207,6 +217,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { write_row(stream, obj, "number", ""); #endif +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + write_row(stream, obj, "select", ""); +#endif + stream->print(F("

See ESPHome Web API for " "REST API documentation.

" "

OTA Update

get_object_id(); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; - obj->dump_json(root); + light::LightJSONSchema::dump_json(*obj, root); }); } #endif @@ -622,6 +637,31 @@ std::string WebServer::number_json(number::Number *obj, float value) { } #endif +#ifdef USE_SELECT +void WebServer::on_select_update(select::Select *obj, const std::string &state) { + this->events_.send(this->select_json(obj, state).c_str(), "state"); +} +void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_selects()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->select_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::select_json(select::Select *obj, const std::string &value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "select-" + obj->get_object_id(); + root["state"] = value; + root["value"] = value; + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -679,6 +719,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_SELECT + if (request->method() == HTTP_GET && match.domain == "select") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -761,6 +806,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_SELECT + if (match.domain == "select") { + this->handle_select_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 4789c6e1c0..54d7356ac9 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -163,6 +163,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string number_json(number::Number *obj, float value); #endif +#ifdef USE_SELECT + void on_select_update(select::Select *obj, const std::string &state) override; + /// Handle a select request under '/select/'. + void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the number state with its value as a JSON string. + std::string select_json(select::Select *obj, const std::string &value); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 37f3c989e4..8f64c473f3 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -25,5 +25,5 @@ async def to_code(config): if CORE.is_esp32: cg.add_library("FS", None) - # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library("ESPAsyncWebServer-esphome", "1.2.7") + # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json + cg.add_library("esphome/ESPAsyncWebServer-esphome", "1.3.0") diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index c51c17d60c..1bccf08a7f 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -142,11 +142,7 @@ IPAddress WiFiComponent::wifi_sta_ip_() { } bool WiFiComponent::wifi_apply_hostname_() { - esp_err_t err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting hostname failed: %d", err); - return false; - } + // setting is done in SYSTEM_EVENT_STA_START callback return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -154,11 +150,25 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (!this->wifi_mode_(true, {})) return false; + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); strcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str()); strcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str()); + // The weakest authmode to accept in the fast scan mode + if (ap.get_password().empty()) { + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; + } else { + conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + } + +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; + } +#endif + if (ap.get_bssid().has_value()) { conf.sta.bssid_set = 1; memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); @@ -167,7 +177,26 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } if (ap.get_channel().has_value()) { conf.sta.channel = *ap.get_channel(); + conf.sta.scan_method = WIFI_FAST_SCAN; + } else { + conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; } + // Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set. + // Units: AP beacon intervals. Defaults to 3 if set to 0. + conf.sta.listen_interval = 0; + +#if ESP_IDF_VERSION_MAJOR >= 4 + // Protected Management Frame + // Device will prefer to connect in PMF mode if other device also advertizes PMF capability. + conf.sta.pmf_cfg.capable = true; + conf.sta.pmf_cfg.required = false; +#endif + + // note, we do our own filtering + // The minimum rssi to accept in the fast scan mode + conf.sta.threshold.rssi = -127; + + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; wifi_config_t current_conf; esp_err_t err; @@ -348,6 +377,8 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Association Failed"; case WIFI_REASON_HANDSHAKE_TIMEOUT: return "Handshake Failed"; + case WIFI_REASON_CONNECTION_FAIL: + return "Connection Failed"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -374,6 +405,7 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i } case SYSTEM_EVENT_STA_START: { ESP_LOGV(TAG, "Event: WiFi STA start"); + tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); break; } case SYSTEM_EVENT_STA_STOP: { @@ -636,6 +668,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { strcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str()); } +#if ESP_IDF_VERSION_MAJOR >= 4 + // pairwise cipher of SoftAP, group cipher will be derived using this. + conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; +#endif + esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index f1807966a2..37bee75928 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, ) @@ -17,11 +16,10 @@ WiFiSignalSensor = wifi_signal_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_DECIBEL_MILLIWATT, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index afff956c9c..690d2f3b00 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -40,7 +40,7 @@ void WLEDLightEffect::stop() { void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { - it[led].set(COLOR_BLACK); + it[led].set(Color::BLACK); } } diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index c736a236a1..f86dc44eeb 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -104,7 +104,7 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult } while (payload_length > 3) { - if (payload[payload_offset + 1] != 0x10) { + if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; } @@ -203,6 +203,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if ((raw[2] == 0x87) && (raw[3] == 0x03)) { // square body, e-ink display result.type = XiaomiParseResult::TYPE_MHOC401; result.name = "MHOC401"; + } else if ((raw[2] == 0x83) && (raw[3] == 0x0A)) { // Qingping-branded, motion & ambient light sensor + result.type = XiaomiParseResult::TYPE_CGPR1; + result.name = "CGPR1"; + if (raw.size() == 19) + result.raw_offset -= 6; } else { ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); return {}; @@ -344,9 +349,9 @@ bool report_xiaomi_results(const optional &result, const std: bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Previously the message was parsed twice per packet, once by XiaomiListener::parse_device() // and then again by the respective device class's parse_device() function. Parsing the header - // here and then for each device seems to be unneccessary and complicates the duplicate packet filtering. + // here and then for each device seems to be unnecessary and complicates the duplicate packet filtering. // Hence I disabled the call to parse_xiaomi_header() here and the message parsing is done entirely - // in the respecive device instance. The XiaomiListener class is defined in __init__.py and I was not + // in the respective device instance. The XiaomiListener class is defined in __init__.py and I was not // able to remove it entirely. return false; // with true it's not showing device scans diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index f431eca11e..7681cbd89c 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -23,7 +23,8 @@ struct XiaomiParseResult { TYPE_MUE4094RT, TYPE_WX08ZM, TYPE_MJYD02YLA, - TYPE_MHOC401 + TYPE_MHOC401, + TYPE_CGPR1 } type; std::string name; optional temperature; diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py index e7f18a6be9..774c87fee9 100644 --- a/esphome/components/xiaomi_cgd1/sensor.py +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py index 6b2c144911..d4e7230fd0 100644 --- a/esphome/components/xiaomi_cgdk2/sensor.py +++ b/esphome/components/xiaomi_cgdk2/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py index f26a7ae54e..4e606d95f8 100644 --- a/esphome/components/xiaomi_cgg1/sensor.py +++ b/esphome/components/xiaomi_cgg1/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgpr1/__init__.py b/esphome/components/xiaomi_cgpr1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py new file mode 100644 index 0000000000..a7f6c41225 --- /dev/null +++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BINDKEY, + CONF_DEVICE_CLASS, + CONF_MAC_ADDRESS, + CONF_ID, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + ICON_EMPTY, + UNIT_PERCENT, + CONF_IDLE_TIME, + CONF_ILLUMINANCE, + UNIT_MINUTE, + UNIT_LUX, + ICON_TIMELAPSE, +) + +DEPENDENCIES = ["esp32_ble_tracker"] +AUTO_LOAD = ["xiaomi_ble", "sensor"] + +xiaomi_cgpr1_ns = cg.esphome_ns.namespace("xiaomi_cgpr1") +XiaomiCGPR1 = xiaomi_cgpr1_ns.class_( + "XiaomiCGPR1", + binary_sensor.BinarySensor, + cg.Component, + esp32_ble_tracker.ESPBTDeviceListener, +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(XiaomiCGPR1), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional( + CONF_DEVICE_CLASS, default="motion" + ): binary_sensor.device_class, + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY + ), + cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( + UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_IDLE_TIME in config: + sens = yield sensor.new_sensor(config[CONF_IDLE_TIME]) + cg.add(var.set_idle_time(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp new file mode 100644 index 0000000000..2ed1024076 --- /dev/null +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp @@ -0,0 +1,79 @@ +#include "xiaomi_cgpr1.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgpr1 { + +static const char *TAG = "xiaomi_cgpr1"; + +void XiaomiCGPR1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGPR1"); + LOG_BINARY_SENSOR(" ", "Motion", this); + LOG_SENSOR(" ", "Idle Time", this->idle_time_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +bool XiaomiCGPR1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->idle_time.has_value() && this->idle_time_ != nullptr) + this->idle_time_->publish_state(*res->idle_time); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + if (res->has_motion.has_value()) + this->publish_state(*res->has_motion); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiCGPR1::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_cgpr1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h new file mode 100644 index 0000000000..0b7369c798 --- /dev/null +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgpr1 { + +class XiaomiCGPR1 : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *idle_time_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; +}; + +} // namespace xiaomi_cgpr1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py index a5c702aa9d..4154b64233 100644 --- a/esphome/components/xiaomi_gcls002/sensor.py +++ b/esphome/components/xiaomi_gcls002/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, ICON_WATER_PERCENT, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -35,32 +33,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiGCLS002), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py index 03289a6219..1818731a0f 100644 --- a/esphome/components/xiaomi_hhccjcy01/sensor.py +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, ICON_WATER_PERCENT, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -37,39 +35,34 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py index 8393de5e5a..82ee12d8d1 100644 --- a/esphome/components/xiaomi_hhccpot002/sensor.py +++ b/esphome/components/xiaomi_hhccpot002/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_WATER_PERCENT, @@ -28,18 +27,16 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py index 70036eb5d9..40991c3d0f 100644 --- a/esphome/components/xiaomi_jqjcy01ym/sensor.py +++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py @@ -7,10 +7,8 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_ID, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -34,32 +32,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MILLIGRAMS_PER_CUBIC_METER, - ICON_FLASK_OUTLINE, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIGRAMS_PER_CUBIC_METER, + icon=ICON_FLASK_OUTLINE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py index ca55f28176..339c5e673a 100644 --- a/esphome/components/xiaomi_lywsd02/sensor.py +++ b/esphome/components/xiaomi_lywsd02/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_BATTERY, @@ -30,25 +29,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiLYWSD02), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py index 05b3798955..f27cee3800 100644 --- a/esphome/components/xiaomi_lywsd03mmc/sensor.py +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -34,25 +33,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py index 82bb4c83fb..39a207327e 100644 --- a/esphome/components/xiaomi_lywsdcgq/sensor.py +++ b/esphome/components/xiaomi_lywsdcgq/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -30,25 +29,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py index 5180bdbb89..57b2190150 100644 --- a/esphome/components/xiaomi_mhoc401/sensor.py +++ b/esphome/components/xiaomi_mhoc401/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -33,25 +32,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py index 9fe76c0645..3a112dfa34 100644 --- a/esphome/components/xiaomi_miscale/sensor.py +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_KILOGRAM, ICON_SCALE_BATHROOM, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -24,11 +23,10 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiMiscale), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_WEIGHT): sensor.sensor_schema( - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOGRAM, + icon=ICON_SCALE_BATHROOM, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index 78464da6e3..36b4c8cc00 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -51,7 +51,7 @@ optional XiaomiMiscale::parse_header(const esp32_ble_tracker::Servi } bool XiaomiMiscale::parse_message(const std::vector &message, ParseResult &result) { - // exemple 1d18 a2 6036 e307 07 11 0f1f11 + // example 1d18 a2 6036 e307 07 11 0f1f11 // 1-2 Weight (MISCALE 181D) // 3-4 Years (MISCALE 181D) // 5 month (MISCALE 181D) diff --git a/esphome/components/xiaomi_miscale2/sensor.py b/esphome/components/xiaomi_miscale2/sensor.py index 9944098407..7cc5984c62 100644 --- a/esphome/components/xiaomi_miscale2/sensor.py +++ b/esphome/components/xiaomi_miscale2/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( UNIT_OHM, CONF_IMPEDANCE, ICON_OMEGA, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -27,14 +26,16 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiMiscale2), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_WEIGHT): sensor.sensor_schema( - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOGRAM, + icon=ICON_SCALE_BATHROOM, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema( - UNIT_OHM, ICON_OMEGA, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_OMEGA, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 90b971c08a..fd4bae60c1 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -9,9 +9,7 @@ from esphome.const import ( CONF_LIGHT, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_PERCENT, @@ -43,21 +41,22 @@ CONFIG_SCHEMA = cv.All( CONF_DEVICE_CLASS, default="motion" ): binary_sensor.device_class, cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( - UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMELAPSE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend( { diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py index 90d4702da4..d2b353beff 100644 --- a/esphome/components/xiaomi_wx08zm/binary_sensor.py +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -6,8 +6,6 @@ from esphome.const import ( CONF_MAC_ADDRESS, CONF_TABLET, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_BUG, @@ -32,14 +30,16 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TABLET): sensor.sensor_schema( - UNIT_PERCENT, ICON_BUG, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_BUG, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index 5f9a5e3add..f2273afa9e 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -9,10 +9,8 @@ from esphome.const import ( CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, @@ -34,21 +32,22 @@ CONFIG_SCHEMA = cv.Schema( pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt ), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + 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_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.polling_component_schema("60s")) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index aad147dbc9..3aebca81b8 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -15,6 +15,7 @@ from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, CONF_COMMAND_TOPIC, + CONF_DISABLED_BY_DEFAULT, CONF_DISCOVERY, CONF_ID, CONF_INTERNAL, @@ -563,6 +564,23 @@ def has_at_most_one_key(*keys): return validate +def has_none_or_all_keys(*keys): + """Validate that none or all of the given keys exist in the config.""" + + def validate(obj): + if not isinstance(obj, dict): + raise Invalid("expected dictionary") + + number = sum(k in keys for k in obj) + if number != 0 and number != len(keys): + raise Invalid( + "Must specify either none or all of {}.".format(", ".join(keys)) + ) + return obj + + return validate + + TIME_PERIOD_ERROR = ( "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h" ) @@ -873,11 +891,20 @@ def validate_bytes(value): def hostname(value): value = string(value) + warned_underscore = False if len(value) > 63: raise Invalid("Hostnames can only be 63 characters long") for c in value: - if not (c.isalnum() or c in "_-"): - raise Invalid("Hostname can only have alphanumeric characters and _ or -") + if not (c.isalnum() or c in "-_"): + raise Invalid("Hostname can only have alphanumeric characters and -") + if c in "_" and not warned_underscore: + _LOGGER.warning( + "'%s': Using the '_' (underscore) character in the hostname is discouraged " + "as it can cause problems with some DHCP and local name services. " + "For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name", + value, + ) + warned_underscore = True return value @@ -1539,17 +1566,14 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema( MQTT_COMPONENT_SCHEMA = Schema( { - Optional(CONF_NAME): string, Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic), Optional(CONF_AVAILABILITY): All( requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA) ), - Optional(CONF_INTERNAL): boolean, } ) -MQTT_COMPONENT_SCHEMA.add_extra(_nameable_validator) MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( { @@ -1557,6 +1581,16 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( } ) +NAMEABLE_SCHEMA = Schema( + { + Optional(CONF_NAME): string, + Optional(CONF_INTERNAL): boolean, + Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, + } +) + +NAMEABLE_SCHEMA.add_extra(_nameable_validator) + COMPONENT_SCHEMA = Schema({Optional(CONF_SETUP_PRIORITY): float_}) diff --git a/esphome/const.py b/esphome/const.py index 6dda61cce2..50bac3ca8d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "1.20.4" +__version__ = "1.22.0-dev" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" @@ -24,6 +24,7 @@ ARDUINO_VERSION_ESP32 = { # See also https://github.com/platformio/platform-espressif8266/releases ARDUINO_VERSION_ESP8266 = { "dev": "https://github.com/platformio/platform-espressif8266.git", + "3.0.1": "platformio/espressif8266@3.1.0", "3.0.0": "platformio/espressif8266@3.0.0", "2.7.4": "platformio/espressif8266@2.6.2", "2.7.3": "platformio/espressif8266@2.6.1", @@ -95,6 +96,7 @@ CONF_BUFFER_SIZE = "buffer_size" CONF_BUILD_PATH = "build_path" CONF_BUS_VOLTAGE = "bus_voltage" CONF_BUSY_PIN = "busy_pin" +CONF_CALCULATED_LUX = "calculated_lux" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" @@ -120,6 +122,7 @@ CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" +CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" @@ -131,9 +134,12 @@ CONF_COMPONENTS = "components" CONF_CONDITION = "condition" CONF_CONDITION_ID = "condition_id" CONF_CONDUCTIVITY = "conductivity" +CONF_CONSTANT_BRIGHTNESS = "constant_brightness" CONF_CONTRAST = "contrast" CONF_COOL_ACTION = "cool_action" +CONF_COOL_DEADBAND = "cool_deadband" CONF_COOL_MODE = "cool_mode" +CONF_COOL_OVERRUN = "cool_overrun" CONF_COUNT = "count" CONF_COUNT_MODE = "count_mode" CONF_COURSE = "course" @@ -157,8 +163,10 @@ CONF_DATA_TEMPLATE = "data_template" CONF_DAYS_OF_MONTH = "days_of_month" CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" +CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr" CONF_DEBOUNCE = "debounce" CONF_DECELERATION = "deceleration" +CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low" CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" @@ -166,11 +174,13 @@ CONF_DELAY = "delay" CONF_DELTA = "delta" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" +CONF_DEVICE_FACTOR = "device_factor" CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" +CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCOVERY = "discovery" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_DISCOVERY_RETAIN = "discovery_retain" @@ -217,7 +227,11 @@ CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action" CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action" CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action" CONF_FAN_ONLY_ACTION = "fan_only_action" +CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER = "fan_only_action_uses_fan_mode_timer" +CONF_FAN_ONLY_COOLING = "fan_only_cooling" CONF_FAN_ONLY_MODE = "fan_only_mode" +CONF_FAN_WITH_COOLING = "fan_with_cooling" +CONF_FAN_WITH_HEATING = "fan_with_heating" CONF_FAST_CONNECT = "fast_connect" CONF_FILE = "file" CONF_FILTER = "filter" @@ -234,11 +248,13 @@ CONF_FORMAT = "format" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" CONF_FREQUENCY = "frequency" CONF_FROM = "from" +CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_UPDATE_EVERY = "full_update_every" CONF_GAIN = "gain" CONF_GAMMA_CORRECT = "gamma_correct" CONF_GAS_RESISTANCE = "gas_resistance" CONF_GATEWAY = "gateway" +CONF_GLASS_ATTENUATION_FACTOR = "glass_attenuation_factor" CONF_GLYPHS = "glyphs" CONF_GPIO = "gpio" CONF_GREEN = "green" @@ -246,7 +262,9 @@ CONF_GROUP = "group" CONF_HARDWARE_UART = "hardware_uart" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" +CONF_HEAT_DEADBAND = "heat_deadband" CONF_HEAT_MODE = "heat_mode" +CONF_HEAT_OVERRUN = "heat_overrun" CONF_HEATER = "heater" CONF_HEIGHT = "height" CONF_HIDDEN = "hidden" @@ -275,7 +293,9 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy" CONF_INCLUDES = "includes" CONF_INDEX = "index" CONF_INDOOR = "indoor" +CONF_INFRARED = "infrared" CONF_INITIAL_MODE = "initial_mode" +CONF_INITIAL_OPTION = "initial_option" CONF_INITIAL_VALUE = "initial_value" CONF_INTEGRATION_TIME = "integration_time" CONF_INTENSITY = "intensity" @@ -320,8 +340,10 @@ CONF_MAKE_ID = "make_id" CONF_MANUAL_IP = "manual_ip" CONF_MANUFACTURER_ID = "manufacturer_id" CONF_MASK_DISTURBER = "mask_disturber" +CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time" CONF_MAX_CURRENT = "max_current" CONF_MAX_DURATION = "max_duration" +CONF_MAX_HEATING_RUN_TIME = "max_heating_run_time" CONF_MAX_LENGTH = "max_length" CONF_MAX_LEVEL = "max_level" CONF_MAX_POWER = "max_power" @@ -335,6 +357,14 @@ CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" CONF_METHOD = "method" +CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time" +CONF_MIN_COOLING_RUN_TIME = "min_cooling_run_time" +CONF_MIN_FAN_MODE_SWITCHING_TIME = "min_fan_mode_switching_time" +CONF_MIN_FANNING_OFF_TIME = "min_fanning_off_time" +CONF_MIN_FANNING_RUN_TIME = "min_fanning_run_time" +CONF_MIN_HEATING_OFF_TIME = "min_heating_off_time" +CONF_MIN_HEATING_RUN_TIME = "min_heating_run_time" +CONF_MIN_IDLE_TIME = "min_idle_time" CONF_MIN_LENGTH = "min_length" CONF_MIN_LEVEL = "min_level" CONF_MIN_POWER = "min_power" @@ -405,6 +435,8 @@ CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt" CONF_OPEN_DURATION = "open_duration" CONF_OPEN_ENDSTOP = "open_endstop" CONF_OPTIMISTIC = "optimistic" +CONF_OPTION = "option" +CONF_OPTIONS = "options" CONF_OR = "or" CONF_OSCILLATING = "oscillating" CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" @@ -436,10 +468,19 @@ CONF_PINS = "pins" CONF_PIXEL_MAPPER = "pixel_mapper" CONF_PLATFORM = "platform" CONF_PLATFORMIO_OPTIONS = "platformio_options" +CONF_PM_0_3UM = "pm_0_3um" +CONF_PM_0_5UM = "pm_0_5um" CONF_PM_1_0 = "pm_1_0" +CONF_PM_1_0_STD = "pm_1_0_std" +CONF_PM_1_0UM = "pm_1_0um" CONF_PM_10_0 = "pm_10_0" +CONF_PM_10_0_STD = "pm_10_0_std" +CONF_PM_10_0UM = "pm_10_0um" CONF_PM_2_5 = "pm_2_5" +CONF_PM_2_5_STD = "pm_2_5_std" +CONF_PM_2_5UM = "pm_2_5um" CONF_PM_4_0 = "pm_4_0" +CONF_PM_5_0UM = "pm_5_0um" CONF_PM_SIZE = "pm_size" CONF_PMC_0_5 = "pmc_0_5" CONF_PMC_1_0 = "pmc_1_0" @@ -527,6 +568,7 @@ CONF_SERVERS = "servers" CONF_SERVICE = "service" CONF_SERVICE_UUID = "service_uuid" CONF_SERVICES = "services" +CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential" CONF_SETUP_MODE = "setup_mode" CONF_SETUP_PRIORITY = "setup_priority" CONF_SHUNT_RESISTANCE = "shunt_resistance" @@ -546,6 +588,7 @@ CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" CONF_SSL_FINGERPRINTS = "ssl_fingerprints" +CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" CONF_STATE_TOPIC = "state_topic" @@ -558,6 +601,10 @@ CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" CONF_SUBNET = "subnet" CONF_SUBSTITUTIONS = "substitutions" +CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" +CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta" +CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action" +CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" CONF_SWING_BOTH_ACTION = "swing_both_action" @@ -572,6 +619,7 @@ CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" CONF_TARGET_TEMPERATURE = "target_temperature" +CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_HIGH = "target_temperature_high" CONF_TARGET_TEMPERATURE_LOW = "target_temperature_low" CONF_TEMPERATURE = "temperature" @@ -619,6 +667,7 @@ CONF_VALUE = "value" CONF_VARIABLES = "variables" CONF_VARIANT = "variant" CONF_VERSION = "version" +CONF_VISIBLE = "visible" CONF_VISUAL = "visual" CONF_VOLTAGE = "voltage" CONF_VOLTAGE_ATTENUATION = "voltage_attenuation" @@ -652,8 +701,10 @@ ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" ICON_BLUETOOTH = "mdi:bluetooth" +ICON_BLUR = "mdi:blur" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" +ICON_BRIGHTNESS_6 = "mdi:brightness-6" ICON_BUG = "mdi:bug" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" @@ -702,6 +753,7 @@ ICON_WIFI = "mdi:wifi" UNIT_AMPERE = "A" UNIT_CELSIUS = "°C" +UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" UNIT_DECIBEL = "dB" @@ -774,6 +826,7 @@ DEVICE_CLASS_CURRENT = "current" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" +DEVICE_CLASS_MONETARY = "monetary" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_POWER_FACTOR = "power_factor" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index df98e1b150..c45d0969e1 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -20,6 +20,7 @@ from esphome.coroutine import FakeEventLoop as _FakeEventLoop from esphome.coroutine import coroutine, coroutine_with_priority # noqa from esphome.helpers import ensure_unique_string, is_hassio from esphome.util import OrderedDict +from esphome import boards if TYPE_CHECKING: from ..cpp_generator import MockObj, MockObjClass, Statement @@ -408,19 +409,28 @@ class Define: class Library: - def __init__(self, name, version): + def __init__(self, name, version, repository=None): self.name = name self.version = version + self.repository = repository + + def __str__(self): + return self.as_lib_dep @property def as_lib_dep(self): + if self.repository is not None: + if self.name is not None: + return f"{self.name}={self.repository}" + return self.repository + if self.version is None: return self.name return f"{self.name}@{self.version}" @property def as_tuple(self): - return self.name, self.version + return self.name, self.version, self.repository def __hash__(self): return hash(self.as_tuple) @@ -584,10 +594,20 @@ class EsphomeCore: @property def is_esp32(self): + """Check if the ESP32 platform is used. + + This checks if the ESP32 platform is in use, which + support ESP32 as well as other chips such as ESP32-C3 + """ if self.esp_platform is None: raise ValueError("No platform specified") return self.esp_platform == "ESP32" + @property + def is_esp32_c3(self): + """Check if the ESP32-C3 SoC is being used.""" + return self.is_esp32 and self.board in boards.ESP32_C3_BOARD_PINS + def add_job(self, func, *args, **kwargs): self.event_loop.add_job(func, *args, **kwargs) @@ -632,10 +652,24 @@ class EsphomeCore: "Library {} must be instance of Library, not {}" "".format(library, type(library)) ) - _LOGGER.debug("Adding library: %s", library) for other in self.libraries[:]: - if other.name != library.name: + if other.name != library.name or other.name is None or library.name is None: continue + if other.repository is not None: + if library.repository is None or other.repository == library.repository: + # Other is using a/the same repository, takes precendence + break + raise ValueError( + "Adding named Library with repository failed! Libraries {} and {} " + "requested with conflicting repositories!" + "".format(library, other) + ) + + if library.repository is not None: + # This is more specific since its using a repository + self.libraries.remove(other) + continue + if library.version is None: # Other requirement is more specific break @@ -652,6 +686,7 @@ class EsphomeCore: "".format(library, other) ) else: + _LOGGER.debug("Adding library: %s", library) self.libraries.append(library) return library diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 17a2725de5..1a3158e4ce 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -53,6 +53,7 @@ void Application::setup() { } this->app_state_ = new_app_state; yield(); + this->feed_wdt(); } while (!component->can_proceed()); } @@ -117,12 +118,7 @@ void ICACHE_RAM_ATTR HOT Application::feed_wdt() { static uint32_t LAST_FEED = 0; uint32_t now = millis(); if (now - LAST_FEED > 3) { -#ifdef ARDUINO_ARCH_ESP8266 - ESP.wdtFeed(); -#endif -#ifdef ARDUINO_ARCH_ESP32 - yield(); -#endif + this->feed_wdt_arch_(); LAST_FEED = now; #ifdef USE_STATUS_LED if (status_led::global_status_led != nullptr) { diff --git a/esphome/core/application.h b/esphome/core/application.h index e065552a74..e5f686a320 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -35,6 +35,9 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif namespace esphome { @@ -89,6 +92,10 @@ class Application { void register_number(number::Number *number) { this->numbers_.push_back(number); } #endif +#ifdef USE_SELECT + void register_select(select::Select *select) { this->selects_.push_back(select); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -224,6 +231,15 @@ class Application { return nullptr; } #endif +#ifdef USE_SELECT + const std::vector &get_selects() { return this->selects_; } + select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->selects_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif Scheduler scheduler; @@ -234,6 +250,8 @@ class Application { void calculate_looping_components_(); + void feed_wdt_arch_(); + std::vector components_{}; std::vector looping_components_{}; @@ -264,6 +282,9 @@ class Application { #ifdef USE_NUMBER std::vector numbers_{}; #endif +#ifdef USE_SELECT + std::vector selects_{}; +#endif std::string name_; std::string compilation_time_; diff --git a/esphome/core/application_esp32.cpp b/esphome/core/application_esp32.cpp new file mode 100644 index 0000000000..9f084428bb --- /dev/null +++ b/esphome/core/application_esp32.cpp @@ -0,0 +1,21 @@ +#include "esphome/core/application.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { + +static const char *const TAG = "app_esp32"; + +void ICACHE_RAM_ATTR HOT Application::feed_wdt_arch_() { +#if CONFIG_ARDUINO_RUNNING_CORE == 0 +#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0 + // ESP32 uses "Task Watchdog" which is hooked to the FreeRTOS idle task. + // To cause the Watchdog to be triggered we need to put the current task + // to sleep to get the idle task scheduled. + delay(1); +#endif +#endif +} + +} // namespace esphome +#endif diff --git a/esphome/core/application_esp8266.cpp b/esphome/core/application_esp8266.cpp new file mode 100644 index 0000000000..95139ca112 --- /dev/null +++ b/esphome/core/application_esp8266.cpp @@ -0,0 +1,12 @@ +#include "esphome/core/application.h" + +#ifdef ARDUINO_ARCH_ESP8266 + +namespace esphome { + +static const char *const TAG = "app_esp8266"; + +void ICACHE_RAM_ATTR HOT Application::feed_wdt_arch_() { ESP.wdtFeed(); } + +} // namespace esphome +#endif diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp new file mode 100644 index 0000000000..58d995db2f --- /dev/null +++ b/esphome/core/color.cpp @@ -0,0 +1,11 @@ +#include "esphome/core/color.h" + +namespace esphome { + +const Color Color::BLACK(0, 0, 0, 0); +const Color Color::WHITE(255, 255, 255, 255); + +const Color COLOR_BLACK(0, 0, 0, 0); +const Color COLOR_WHITE(255, 255, 255, 255); + +} // namespace esphome diff --git a/esphome/core/color.h b/esphome/core/color.h index 6e8c769d10..c9ca3bcfc3 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -38,10 +38,10 @@ struct Color { g(green), b(blue), w(white) {} - inline Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), - g((colorcode >> 8) & 0xFF), - b((colorcode >> 0) & 0xFF), - w((colorcode >> 24) & 0xFF) {} + inline explicit Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), + g((colorcode >> 8) & 0xFF), + b((colorcode >> 0) & 0xFF), + w((colorcode >> 24) & 0xFF) {} inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } inline Color &operator=(const Color &rhs) ALWAYS_INLINE { // NOLINT @@ -143,8 +143,14 @@ struct Color { Color fade_to_black(uint8_t amnt) { return *this * amnt; } Color lighten(uint8_t delta) { return *this + delta; } Color darken(uint8_t delta) { return *this - delta; } + + static const Color BLACK; + static const Color WHITE; }; -static const Color COLOR_BLACK(0, 0, 0); -static const Color COLOR_WHITE(255, 255, 255, 255); -}; // namespace esphome +ESPDEPRECATED("Use Color::BLACK instead of COLOR_BLACK", "v1.21") +extern const Color COLOR_BLACK; +ESPDEPRECATED("Use Color::WHITE instead of COLOR_WHITE", "v1.21") +extern const Color COLOR_WHITE; + +} // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 09c91fbb0c..f6b15b1977 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -187,4 +187,7 @@ void Nameable::calc_object_id_() { } uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; } +bool Nameable::is_disabled_by_default() const { return this->disabled_by_default_; } +void Nameable::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; } + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 001620fe4a..a4a945ef2a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -22,7 +22,7 @@ extern const float IO; extern const float HARDWARE; /// For components that import data from directly connected sensors like DHT. extern const float DATA; -/// Alias for DATA (here for compatability reasons) +/// Alias for DATA (here for compatibility reasons) extern const float HARDWARE_LATE; /// For components that use data from sensors like displays extern const float PROCESSOR; @@ -256,6 +256,14 @@ class Nameable { bool is_internal() const; void set_internal(bool internal); + /** Check if this object is declared to be disabled by default. + * + * That means that when the device gets added to Home Assistant (or other clients) it should + * not be added to the default view by default, and a user action is necessary to manually add it. + */ + bool is_disabled_by_default() const; + void set_disabled_by_default(bool disabled_by_default); + protected: virtual uint32_t hash_base() = 0; @@ -265,6 +273,7 @@ class Nameable { std::string object_id_; uint32_t object_id_hash_; bool internal_{false}; + bool disabled_by_default_{false}; }; } // namespace esphome diff --git a/esphome/core/config.py b/esphome/core/config.py index 9475225f4d..45b8809f1e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -4,7 +4,7 @@ import re import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation, pins +from esphome import automation, boards from esphome.const import ( CONF_ARDUINO_VERSION, CONF_BOARD, @@ -50,18 +50,19 @@ VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$") CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix" -def validate_board(value): +def validate_board(value: str): if CORE.is_esp8266: - board_pins = pins.ESP8266_BOARD_PINS + boardlist = boards.ESP8266_BOARD_PINS.keys() elif CORE.is_esp32: - board_pins = pins.ESP32_BOARD_PINS + boardlist = list(boards.ESP32_BOARD_PINS.keys()) + boardlist += list(boards.ESP32_C3_BOARD_PINS.keys()) else: raise NotImplementedError - if value not in board_pins: + if value not in boardlist: raise cv.Invalid( "Could not find board '{}'. Valid boards are {}".format( - value, ", ".join(sorted(board_pins.keys())) + value, ", ".join(sorted(boardlist)) ) ) return value @@ -157,7 +158,7 @@ def valid_project_name(value: str): CONFIG_SCHEMA = cv.Schema( { - cv.Required(CONF_NAME): cv.valid_name, + cv.Required(CONF_NAME): cv.hostname, cv.Required(CONF_PLATFORM): cv.one_of("ESP8266", "ESP32", upper=True), cv.Required(CONF_BOARD): validate_board, cv.Optional(CONF_COMMENT): cv.string, @@ -332,9 +333,26 @@ async def to_code(config): if "@" in lib: name, vers = lib.split("@", 1) cg.add_library(name, vers) + elif "://" in lib: + # Repository... + if "=" in lib: + name, repo = lib.split("=", 1) + cg.add_library(name, None, repo) + else: + cg.add_library(None, None, lib) + else: cg.add_library(lib, None) + if CORE.is_esp8266: + # 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 + # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of + # a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3, + # which always aborts if exceptions are disabled. + # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` + cg.add_build_flag("-DNEW_OOM_ABORT") + cg.add_build_flag("-Wno-unused-variable") cg.add_build_flag("-Wno-unused-but-set-variable") cg.add_build_flag("-Wno-sign-compare") diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 305fe93532..1d25be41f2 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -59,6 +59,12 @@ void Controller::setup_controller() { obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); } #endif +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 746658075f..0de8f7ea19 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -28,6 +28,9 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif namespace esphome { @@ -61,6 +64,9 @@ class Controller { #ifdef USE_NUMBER virtual void on_number_update(number::Number *obj, float state){}; #endif +#ifdef USE_SELECT + virtual void on_select_update(select::Select *obj, const std::string &state){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index cac03fc703..5c176d1b33 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -14,6 +14,7 @@ #define USE_LIGHT #define USE_CLIMATE #define USE_NUMBER +#define USE_SELECT #define USE_MQTT #define USE_POWER_SUPPLY #define USE_HOMEASSISTANT_TIME diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index d7adc8dbf1..6b8350991e 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -27,11 +27,16 @@ GPIOPin::GPIOPin(uint8_t pin, uint8_t mode, bool inverted) #ifdef ARDUINO_ARCH_ESP8266 gpio_read_(pin < 16 ? &GPI : &GP16I), gpio_mask_(pin < 16 ? (1UL << pin) : 1) -#endif -#ifdef ARDUINO_ARCH_ESP32 - gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), +#elif ARDUINO_ARCH_ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32C3 + gpio_set_(&GPIO.out_w1ts.val), + gpio_clear_(&GPIO.out_w1tc.val), + gpio_read_(&GPIO.in.val), +#else + gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), gpio_clear_(pin < 32 ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val), gpio_read_(pin < 32 ? &GPIO.in : &GPIO.in1.val), +#endif gpio_mask_(pin < 32 ? (1UL << pin) : (1UL << (pin - 32))) #endif { @@ -194,12 +199,16 @@ void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() { GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, this->gpio_mask_); #endif #ifdef ARDUINO_ARCH_ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32C3 + GPIO.status_w1tc.val = this->gpio_mask_; +#else if (this->pin_ < 32) { GPIO.status_w1tc = this->gpio_mask_; } else { GPIO.status1_w1tc.intr_st = this->gpio_mask_; } #endif +#endif } void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a6cf8b779c..9e9c775899 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -79,6 +79,15 @@ float gamma_correct(float value, float gamma) { return powf(value, gamma); } +float gamma_uncorrect(float value, float gamma) { + if (value <= 0.0f) + return 0.0f; + if (gamma <= 0.0f) + return value; + + return powf(value, 1 / gamma); +} + std::string to_lowercase_underscore(std::string s) { std::transform(s.begin(), s.end(), s.begin(), ::tolower); std::replace(s.begin(), s.end(), ' ', '_'); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 808f96d4b8..5868918cd6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -17,7 +17,7 @@ #endif #define HOT __attribute__((hot)) -#define ESPDEPRECATED(msg) __attribute__((deprecated(msg))) +#define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg))) #define ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) @@ -116,6 +116,8 @@ uint8_t fast_random_8(); /// Applies gamma correction with the provided gamma to value. float gamma_correct(float value, float gamma); +/// Reverts gamma correction with the provided gamma to value. +float gamma_uncorrect(float value, float gamma); /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 802e9a9d38..eda378e5eb 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -543,13 +543,13 @@ def add_global(expression: Union[SafeExpType, Statement]): CORE.add_global(expression) -def add_library(name: str, version: Optional[str]): +def add_library(name: str, version: Optional[str], repository: Optional[str] = None): """Add a library to the codegen library storage. :param name: The name of the library (for example 'AsyncTCP') :param version: The version of the library, may be None. """ - CORE.add_library(Library(name, version)) + CORE.add_library(Library(name, version, repository)) def add_build_flag(build_flag: str): diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 66f72bfc00..ea55ea3b18 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -98,7 +98,7 @@ class DashboardSettings: return os.path.join(self.config_dir, *args) def list_yaml_files(self): - return util.list_yaml_files(self.config_dir) + return util.list_yaml_files([self.config_dir]) settings = DashboardSettings() @@ -329,7 +329,7 @@ class EsphomeVscodeHandler(EsphomeCommandWebSocket): class EsphomeAceEditorHandler(EsphomeCommandWebSocket): def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "vscode", settings.config_dir, "--ace"] + return ["esphome", "--dashboard", "-q", "vscode", "--ace", settings.config_dir] class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 47071b5391..199c68210e 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -1,4 +1,4 @@ -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod from typing import Dict, Any import contextvars @@ -14,7 +14,8 @@ from esphome.core import CORE class FinalValidateConfig(ABC): - @abstractproperty + @property + @abstractmethod def data(self) -> Dict[str, Any]: """A dictionary that can be used by post validation functions to store global data during the validation phase. Each component should store its diff --git a/esphome/legacy.py b/esphome/legacy.py deleted file mode 100644 index 6b3b1d6c99..0000000000 --- a/esphome/legacy.py +++ /dev/null @@ -1,12 +0,0 @@ -import sys - - -def main(): - print("The esphomeyaml command has been renamed to esphome.") - print("") - print("$ esphome {}".format(" ".join(sys.argv[1:]))) - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/esphome/pins.py b/esphome/pins.py index e314e3fc30..ff4ed9d9c1 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -4,877 +4,22 @@ import esphome.config_validation as cv from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER from esphome.core import CORE from esphome.util import SimpleRegistry +from esphome import boards _LOGGER = logging.getLogger(__name__) -ESP8266_BASE_PINS = { - "A0": 17, - "SS": 15, - "MOSI": 13, - "MISO": 12, - "SCK": 14, - "SDA": 4, - "SCL": 5, - "RX": 3, - "TX": 1, -} - -ESP8266_BOARD_PINS = { - "d1": { - "D0": 3, - "D1": 1, - "D2": 16, - "D3": 5, - "D4": 4, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 0, - "D9": 2, - "D10": 15, - "D11": 13, - "D12": 14, - "D13": 14, - "D14": 4, - "D15": 5, - "LED": 2, - }, - "d1_mini": { - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "LED": 2, - }, - "d1_mini_lite": "d1_mini", - "d1_mini_pro": "d1_mini", - "esp01": {}, - "esp01_1m": {}, - "esp07": {}, - "esp12e": {}, - "esp210": {}, - "esp8285": {}, - "esp_wroom_02": {}, - "espduino": {"LED": 16}, - "espectro": {"LED": 15, "BUTTON": 2}, - "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, - "espinotee": {"LED": 16}, - "espresso_lite_v1": {"LED": 16}, - "espresso_lite_v2": {"LED": 2}, - "gen4iod": {}, - "heltec_wifi_kit_8": "d1_mini", - "huzzah": { - "LED": 0, - "LED_RED": 0, - "LED_BLUE": 2, - "D4": 4, - "D5": 5, - "D12": 12, - "D13": 13, - "D14": 14, - "D15": 15, - "D16": 16, - }, - "inventone": {}, - "modwifi": {}, - "nodemcu": { - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "D10": 1, - "LED": 16, - }, - "nodemcuv2": "nodemcu", - "oak": { - "P0": 2, - "P1": 5, - "P2": 0, - "P3": 3, - "P4": 1, - "P5": 4, - "P6": 15, - "P7": 13, - "P8": 12, - "P9": 14, - "P10": 16, - "P11": 17, - "LED": 5, - }, - "phoenix_v1": {"LED": 16}, - "phoenix_v2": {"LED": 2}, - "sparkfunBlynk": "thing", - "thing": {"LED": 5, "SDA": 2, "SCL": 14}, - "thingdev": "thing", - "wifi_slot": {"LED": 2}, - "wifiduino": { - "D0": 3, - "D1": 1, - "D2": 2, - "D3": 0, - "D4": 4, - "D5": 5, - "D6": 16, - "D7": 14, - "D8": 12, - "D9": 13, - "D10": 15, - "D11": 13, - "D12": 12, - "D13": 14, - }, - "wifinfo": { - "LED": 12, - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "D10": 1, - }, - "wio_link": {"LED": 2, "GROVE": 15, "D0": 14, "D1": 12, "D2": 13, "BUTTON": 0}, - "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, - "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, -} - -FLASH_SIZE_1_MB = 2 ** 20 -FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 -FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB -FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB -FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB - -ESP8266_FLASH_SIZES = { - "d1": FLASH_SIZE_4_MB, - "d1_mini": FLASH_SIZE_4_MB, - "d1_mini_lite": FLASH_SIZE_1_MB, - "d1_mini_pro": FLASH_SIZE_16_MB, - "esp01": FLASH_SIZE_512_KB, - "esp01_1m": FLASH_SIZE_1_MB, - "esp07": FLASH_SIZE_4_MB, - "esp12e": FLASH_SIZE_4_MB, - "esp210": FLASH_SIZE_4_MB, - "esp8285": FLASH_SIZE_1_MB, - "esp_wroom_02": FLASH_SIZE_2_MB, - "espduino": FLASH_SIZE_4_MB, - "espectro": FLASH_SIZE_4_MB, - "espino": FLASH_SIZE_4_MB, - "espinotee": FLASH_SIZE_4_MB, - "espresso_lite_v1": FLASH_SIZE_4_MB, - "espresso_lite_v2": FLASH_SIZE_4_MB, - "gen4iod": FLASH_SIZE_512_KB, - "heltec_wifi_kit_8": FLASH_SIZE_4_MB, - "huzzah": FLASH_SIZE_4_MB, - "inventone": FLASH_SIZE_4_MB, - "modwifi": FLASH_SIZE_2_MB, - "nodemcu": FLASH_SIZE_4_MB, - "nodemcuv2": FLASH_SIZE_4_MB, - "oak": FLASH_SIZE_4_MB, - "phoenix_v1": FLASH_SIZE_4_MB, - "phoenix_v2": FLASH_SIZE_4_MB, - "sparkfunBlynk": FLASH_SIZE_4_MB, - "thing": FLASH_SIZE_512_KB, - "thingdev": FLASH_SIZE_512_KB, - "wifi_slot": FLASH_SIZE_1_MB, - "wifiduino": FLASH_SIZE_4_MB, - "wifinfo": FLASH_SIZE_1_MB, - "wio_link": FLASH_SIZE_4_MB, - "wio_node": FLASH_SIZE_4_MB, - "xinabox_cw01": FLASH_SIZE_4_MB, -} - -ESP8266_LD_SCRIPTS = { - FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), - FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), - FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), - FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), - FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), -} - -ESP32_BASE_PINS = { - "TX": 1, - "RX": 3, - "SDA": 21, - "SCL": 22, - "SS": 5, - "MOSI": 23, - "MISO": 19, - "SCK": 18, - "A0": 36, - "A3": 39, - "A4": 32, - "A5": 33, - "A6": 34, - "A7": 35, - "A10": 4, - "A11": 0, - "A12": 2, - "A13": 15, - "A14": 13, - "A15": 12, - "A16": 14, - "A17": 27, - "A18": 25, - "A19": 26, - "T0": 4, - "T1": 0, - "T2": 2, - "T3": 15, - "T4": 13, - "T5": 12, - "T6": 14, - "T7": 27, - "T8": 33, - "T9": 32, - "DAC1": 25, - "DAC2": 26, - "SVP": 36, - "SVN": 39, -} - -ESP32_BOARD_PINS = { - "alksesp32": { - "A0": 32, - "A1": 33, - "A2": 25, - "A3": 26, - "A4": 27, - "A5": 14, - "A6": 12, - "A7": 15, - "D0": 40, - "D1": 41, - "D10": 19, - "D11": 21, - "D12": 22, - "D13": 23, - "D2": 15, - "D3": 2, - "D4": 0, - "D5": 4, - "D6": 16, - "D7": 17, - "D8": 5, - "D9": 18, - "DHT_PIN": 26, - "LED": 23, - "L_B": 5, - "L_G": 17, - "L_R": 22, - "L_RGB_B": 16, - "L_RGB_G": 21, - "L_RGB_R": 4, - "L_Y": 23, - "MISO": 22, - "MOSI": 21, - "PHOTO": 25, - "PIEZO1": 19, - "PIEZO2": 18, - "POT1": 32, - "POT2": 33, - "S1": 4, - "S2": 16, - "S3": 18, - "S4": 19, - "S5": 21, - "SCK": 23, - "SCL": 14, - "SDA": 27, - "SS": 19, - "SW1": 15, - "SW2": 2, - "SW3": 0, - }, - "bpi-bit": { - "BUTTON_A": 35, - "BUTTON_B": 27, - "BUZZER": 25, - "LIGHT_SENSOR1": 36, - "LIGHT_SENSOR2": 39, - "MPU9250_INT": 0, - "P0": 25, - "P1": 32, - "P10": 26, - "P11": 27, - "P12": 2, - "P13": 18, - "P14": 19, - "P15": 23, - "P16": 5, - "P19": 22, - "P2": 33, - "P20": 21, - "P3": 13, - "P4": 15, - "P5": 35, - "P6": 12, - "P7": 14, - "P8": 16, - "P9": 17, - "RGB_LED": 4, - "TEMPERATURE_SENSOR": 34, - }, - "d-duino-32": { - "D1": 5, - "D10": 1, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "MISO": 12, - "MOSI": 13, - "SCK": 14, - "SCL": 4, - "SDA": 5, - "SS": 15, - }, - "esp-wrover-kit": {}, - "esp32-devkitlipo": {}, - "esp32-evb": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - "SS": 17, - }, - "esp32-gateway": {"BUTTON": 34, "LED": 33, "SCL": 16, "SDA": 32}, - "esp32-poe-iso": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - }, - "esp32-poe": {"BUTTON": 34, "MISO": 15, "MOSI": 2, "SCK": 14, "SCL": 16, "SDA": 13}, - "esp32-pro": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - "SS": 17, - }, - "esp320": { - "LED": 5, - "MISO": 12, - "MOSI": 13, - "SCK": 14, - "SCL": 14, - "SDA": 2, - "SS": 15, - }, - "esp32cam": {}, - "esp32dev": {}, - "esp32doit-devkit-v1": {"LED": 2}, - "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, - "esp32vn-iot-uno": {}, - "espea32": {"BUTTON": 0, "LED": 5}, - "espectro32": {"LED": 15, "SD_SS": 33}, - "espino32": {"BUTTON": 0, "LED": 16}, - "featheresp32": { - "A0": 26, - "A1": 25, - "A10": 27, - "A11": 12, - "A12": 13, - "A13": 35, - "A2": 34, - "A4": 36, - "A5": 4, - "A6": 14, - "A7": 32, - "A8": 15, - "A9": 33, - "Ax": 2, - "LED": 13, - "MOSI": 18, - "RX": 16, - "SCK": 5, - "SDA": 23, - "SS": 33, - "TX": 17, - }, - "firebeetle32": {"LED": 2}, - "fm-devkit": { - "D0": 34, - "D1": 35, - "D10": 0, - "D2": 32, - "D3": 33, - "D4": 27, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 23, - "I2S_DOUT": 22, - "I2S_LRCLK": 25, - "I2S_MCLK": 2, - "I2S_SCLK": 26, - "LED": 5, - "SCL": 17, - "SDA": 16, - "SW1": 4, - "SW2": 18, - "SW3": 19, - "SW4": 21, - }, - "frogboard": {}, - "heltec_wifi_kit_32": { - "A1": 37, - "A2": 38, - "BUTTON": 0, - "LED": 25, - "RST_OLED": 16, - "SCL_OLED": 15, - "SDA_OLED": 4, - "Vext": 21, - }, - "heltec_wifi_lora_32": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 33, - "DIO2": 32, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "heltec_wifi_lora_32_V2": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 35, - "DIO2": 34, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "heltec_wireless_stick": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 35, - "DIO2": 34, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "hornbill32dev": {"BUTTON": 0, "LED": 13}, - "hornbill32minima": {"SS": 2}, - "intorobot": { - "A1": 39, - "A2": 35, - "A3": 25, - "A4": 26, - "A5": 14, - "A6": 12, - "A7": 15, - "A8": 13, - "A9": 2, - "BUTTON": 0, - "D0": 19, - "D1": 23, - "D2": 18, - "D3": 17, - "D4": 16, - "D5": 5, - "D6": 4, - "LED": 4, - "MISO": 17, - "MOSI": 16, - "RGB_B_BUILTIN": 22, - "RGB_G_BUILTIN": 21, - "RGB_R_BUILTIN": 27, - "SCL": 19, - "SDA": 23, - "T0": 19, - "T1": 23, - "T2": 18, - "T3": 17, - "T4": 16, - "T5": 5, - "T6": 4, - }, - "iotaap_magnolia": {}, - "iotbusio": {}, - "iotbusproteus": {}, - "lolin32": {"LED": 5}, - "lolin32_lite": {"LED": 22}, - "lolin_d32": {"LED": 5, "_VBAT": 35}, - "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, - "lopy": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 17, - }, - "lopy4": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 18, - }, - "m5stack-core-esp32": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - "RXD2": 16, - "TXD2": 17, - }, - "m5stack-fire": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - }, - "m5stack-grey": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - "RXD2": 16, - "TXD2": 17, - }, - "m5stick-c": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G10": 10, - "G26": 26, - "G32": 32, - "G33": 33, - "G36": 36, - "G37": 37, - "G39": 39, - "G9": 9, - "MISO": 36, - "MOSI": 15, - "SCK": 13, - "SCL": 33, - "SDA": 32, - }, - "magicbit": { - "BLUE_LED": 17, - "BUZZER": 25, - "GREEN_LED": 16, - "LDR": 36, - "LED": 16, - "LEFT_BUTTON": 35, - "MOTOR1A": 27, - "MOTOR1B": 18, - "MOTOR2A": 16, - "MOTOR2B": 17, - "POT": 39, - "RED_LED": 27, - "RIGHT_PUTTON": 34, - "YELLOW_LED": 18, - }, - "mhetesp32devkit": {"LED": 2}, - "mhetesp32minikit": {"LED": 2}, - "microduino-core-esp32": { - "A0": 12, - "A1": 13, - "A10": 25, - "A11": 26, - "A12": 27, - "A13": 14, - "A2": 15, - "A3": 4, - "A6": 38, - "A7": 37, - "A8": 32, - "A9": 33, - "D0": 3, - "D1": 1, - "D10": 5, - "D11": 23, - "D12": 19, - "D13": 18, - "D14": 12, - "D15": 13, - "D16": 15, - "D17": 4, - "D18": 22, - "D19": 21, - "D2": 16, - "D20": 38, - "D21": 37, - "D3": 17, - "D4": 32, - "D5": 33, - "D6": 25, - "D7": 26, - "D8": 27, - "D9": 14, - "SCL": 21, - "SCL1": 13, - "SDA": 22, - "SDA1": 12, - }, - "nano32": {"BUTTON": 0, "LED": 16}, - "nina_w10": { - "D0": 3, - "D1": 1, - "D10": 5, - "D11": 19, - "D12": 23, - "D13": 18, - "D14": 13, - "D15": 12, - "D16": 32, - "D17": 33, - "D18": 21, - "D19": 34, - "D2": 26, - "D20": 36, - "D21": 39, - "D3": 25, - "D4": 35, - "D5": 27, - "D6": 22, - "D7": 0, - "D8": 15, - "D9": 14, - "LED_BLUE": 21, - "LED_GREEN": 33, - "LED_RED": 23, - "SCL": 13, - "SDA": 12, - "SW1": 33, - "SW2": 27, - }, - "node32s": {}, - "nodemcu-32s": {"BUTTON": 0, "LED": 2}, - "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, - "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, - "oroca_edubot": { - "A0": 34, - "A1": 39, - "A2": 36, - "A3": 33, - "D0": 4, - "D1": 16, - "D2": 17, - "D3": 22, - "D4": 23, - "D5": 5, - "D6": 18, - "D7": 19, - "D8": 33, - "LED": 13, - "MOSI": 18, - "RX": 16, - "SCK": 5, - "SDA": 23, - "SS": 2, - "TX": 17, - "VBAT": 35, - }, - "pico32": {}, - "pocket_32": {"LED": 16}, - "pycom_gpy": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 17, - }, - "quantum": {}, - "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, - "tinypico": {}, - "ttgo-lora32-v1": { - "A1": 37, - "A2": 38, - "BUTTON": 0, - "LED": 2, - "MOSI": 27, - "SCK": 5, - "SS": 18, - }, - "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, - "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, - "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, - "ttgo-t7-v13-mini32": {"LED": 22}, - "ttgo-t7-v14-mini32": {"LED": 19}, - "turta_iot_node": {}, - "vintlabs-devkit-v1": { - "LED": 2, - "PWM0": 12, - "PWM1": 13, - "PWM2": 14, - "PWM3": 15, - "PWM4": 16, - "PWM5": 17, - "PWM6": 18, - "PWM7": 19, - }, - "wemos_d1_mini32": { - "D0": 26, - "D1": 22, - "D2": 21, - "D3": 17, - "D4": 16, - "D5": 18, - "D6": 19, - "D7": 23, - "D8": 5, - "LED": 2, - "RXD": 3, - "TXD": 1, - "_VBAT": 35, - }, - "wemosbat": {"LED": 16}, - "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, - "widora-air": { - "A1": 39, - "A2": 35, - "A3": 25, - "A4": 26, - "A5": 14, - "A6": 12, - "A7": 15, - "A8": 13, - "A9": 2, - "BUTTON": 0, - "D0": 19, - "D1": 23, - "D2": 18, - "D3": 17, - "D4": 16, - "D5": 5, - "D6": 4, - "LED": 25, - "MISO": 17, - "MOSI": 16, - "SCL": 19, - "SDA": 23, - "T0": 19, - "T1": 23, - "T2": 18, - "T3": 17, - "T4": 16, - "T5": 5, - "T6": 4, - }, - "xinabox_cw02": {"LED": 27}, -} - def _lookup_pin(value): if CORE.is_esp8266: - board_pins_dict = ESP8266_BOARD_PINS - base_pins = ESP8266_BASE_PINS + board_pins_dict = boards.ESP8266_BOARD_PINS + base_pins = boards.ESP8266_BASE_PINS elif CORE.is_esp32: - board_pins_dict = ESP32_BOARD_PINS - base_pins = ESP32_BASE_PINS + if CORE.board in boards.ESP32_C3_BOARD_PINS: + board_pins_dict = boards.ESP32_C3_BOARD_PINS + base_pins = boards.ESP32_C3_BASE_PINS + else: + board_pins_dict = boards.ESP32_BOARD_PINS + base_pins = boards.ESP32_BASE_PINS else: raise NotImplementedError @@ -915,9 +60,27 @@ _ESP_SDIO_PINS = { 11: "Flash Command", } +_ESP32C3_SDIO_PINS = { + 12: "Flash IO3/HOLD#", + 13: "Flash IO2/WP#", + 14: "Flash CS#", + 15: "Flash CLK", + 16: "Flash IO0/DI", + 17: "Flash IO1/DO", +} + def validate_gpio_pin(value): value = _translate_pin(value) + if CORE.is_esp32_c3: + if value < 0 or value > 22: + raise cv.Invalid(f"ESP32-C3: Invalid pin number: {value}") + if value in _ESP32C3_SDIO_PINS: + raise cv.Invalid( + "This pin cannot be used on ESP32-C3s and is already used by " + "the flash interface (function: {})".format(_ESP_SDIO_PINS[value]) + ) + return value if CORE.is_esp32: if value < 0 or value > 39: raise cv.Invalid(f"ESP32: Invalid pin number: {value}") @@ -995,6 +158,10 @@ def output_pin(value): def analog_pin(value): value = validate_gpio_pin(value) if CORE.is_esp32: + if CORE.is_esp32_c3: + if 0 <= value <= 4: # ADC1 + return value + raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") if 32 <= value <= 39: # ADC1 return value raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 87ca12c9a8..3d7e9a6d48 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -202,6 +202,8 @@ STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})") STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})") STACKTRACE_ESP32_PC_RE = re.compile(r"PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_BAD_ALLOC_RE = re.compile( r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" ) @@ -228,6 +230,9 @@ def process_stacktrace(config, line, backtrace_state): # ESP32 PC/EXCVADDR _parse_register(config, STACKTRACE_ESP32_PC_RE, line) _parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line) + # ESP32-C3 PC/RA + _parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line) + _parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line) # bad alloc match = re.match(STACKTRACE_BAD_ALLOC_RE, line) diff --git a/esphome/util.py b/esphome/util.py index 10f9923c44..56bc97ca71 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -247,17 +247,24 @@ class OrderedDict(collections.OrderedDict): return dict(self).__repr__() -def list_yaml_files(folder): - files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) +def list_yaml_files(folders): + files = filter_yaml_files( + [os.path.join(folder, p) for folder in folders for p in os.listdir(folder)] + ) files.sort() return files def filter_yaml_files(files): - files = [f for f in files if os.path.splitext(f)[1] == ".yaml"] - files = [f for f in files if os.path.basename(f) != "secrets.yaml"] - files = [f for f in files if not os.path.basename(f).startswith(".")] - return files + return [ + f + for f in files + if ( + os.path.splitext(f)[1] == ".yaml" + and os.path.basename(f) != "secrets.yaml" + and not os.path.basename(f).startswith(".") + ) + ] class SerialPort: diff --git a/esphome/vscode.py b/esphome/vscode.py index 6e1a0270be..68d59abd02 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -67,7 +67,7 @@ def read_config(args): CORE.ace = args.ace f = data["file"] if CORE.ace: - CORE.config_path = os.path.join(args.configuration[0], f) + CORE.config_path = os.path.join(args.configuration, f) else: CORE.config_path = data["file"] vs = VSCodeResult() diff --git a/esphome/wizard.py b/esphome/wizard.py index 0d912e4bbf..3f989dd93d 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -10,7 +10,7 @@ from esphome.helpers import get_bool_env, write_file from esphome.log import color, Fore # pylint: disable=anomalous-backslash-in-string -from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS +from esphome.boards import ESP32_BOARD_PINS, ESP8266_BOARD_PINS from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_print from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD diff --git a/esphome/writer.py b/esphome/writer.py index 57698f8c25..641ae9b3cc 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -25,7 +25,7 @@ from esphome.helpers import ( get_bool_env, ) from esphome.storage_json import StorageJSON, storage_path -from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS +from esphome.boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS from esphome import loader _LOGGER = logging.getLogger(__name__) @@ -72,9 +72,7 @@ upload_flags = """, ) -UPLOAD_SPEED_OVERRIDE = { - "esp210": 57600, -} +UPLOAD_SPEED_OVERRIDE = {"esp210": 57600} def get_flags(key): @@ -210,11 +208,12 @@ def gather_lib_deps(): return [x.as_lib_dep for x in CORE.libraries] -def gather_build_flags(): - build_flags = CORE.build_flags +def gather_build_flags(overrides): + build_flags = list(CORE.build_flags) + build_flags += [overrides] if isinstance(overrides, str) else overrides # avoid changing build flags order - return list(sorted(list(build_flags))) + return list(sorted(build_flags)) ESP32_LARGE_PARTITIONS_CSV = """\ @@ -228,8 +227,10 @@ spiffs, data, spiffs, 0x391000, 0x00F000 def get_ini_content(): + overrides = CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {}) + lib_deps = gather_lib_deps() - build_flags = gather_build_flags() + build_flags = gather_build_flags(overrides.pop("build_flags", [])) data = { "platform": CORE.arduino_version, @@ -275,7 +276,7 @@ def get_ini_content(): # Ignore libraries that are not explicitly used, but may # be added by LDF # data['lib_ldf_mode'] = 'chain' - data.update(CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {})) + data.update(overrides) content = f"[env:{CORE.name}]\n" content += format_ini(data) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f98bb272b8..10417e6de4 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -411,17 +411,19 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:str", value=str(value)) - # pylint: disable=arguments-differ + # pylint: disable=arguments-renamed def represent_bool(self, value): return self.represent_scalar( "tag:yaml.org,2002:bool", "true" if value else "false" ) + # pylint: disable=arguments-renamed def represent_int(self, value): if is_secret(value): return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:int", value=str(value)) + # pylint: disable=arguments-renamed def represent_float(self, value): if is_secret(value): return self.represent_secret(value) diff --git a/platformio.ini b/platformio.ini index ba7724ad24..c280c54a21 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,35 +1,50 @@ -; This file is so that the C++ files in this repo -; can be edited with IDEs like VSCode or CLion -; with the platformio system +; This PlatformIO project is for development purposes *only*: clang-tidy derives its compilation +; database from here, and IDEs like CLion and VSCode also use it. This does not actually create a +; usable binary. ; It's *not* used during runtime. [platformio] -default_envs = livingroom8266 +default_envs = esp8266 src_dir = . -include_dir = include +include_dir = + +[runtime] +; This are the flags as set by the runtime. +build_flags = + -Wno-unused-variable + -Wno-unused-but-set-variable + -Wno-sign-compare + +[clangtidy] +; This are the flags for clang-tidy. +build_flags = + -Wall + -Wunreachable-code + -Wfor-loop-analysis + -Wshadow-field + -Wshadow-field-in-constructor [common] lib_deps = AsyncMqttClient-esphome@0.8.4 ArduinoJson-esphomelib@5.13.3 - ESPAsyncWebServer-esphome@1.2.7 + esphome/ESPAsyncWebServer-esphome@1.3.0 FastLED@3.3.2 NeoPixelBus-esphome@2.6.2 1655@1.0.2 ; TinyGPSPlus (has name conflict) 6865@1.0.0 ; TM1651 Battery Display 6306@1.0.3 ; HM3301 + glmnet/Dsmr@0.3 ; used by dsmr + rweather/Crypto@0.2.0 ; used by dsmr + build_flags = - -fno-exceptions - -Wno-sign-compare - -Wno-unused-but-set-variable - -Wno-unused-variable - -DCLANG_TIDY -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = + + + +<.temp/all-include.cpp> -[env:livingroom8266] +[common:esp8266] ; use Arduino framework v2.4.2 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760) platform = platformio/espressif8266@1.8.0 framework = arduino @@ -42,7 +57,7 @@ lib_deps = build_flags = ${common.build_flags} src_filter = ${common.src_filter} -[env:livingroom32] +[common:esp32] platform = platformio/espressif32@3.2.0 framework = arduino board = nodemcu-32s @@ -56,3 +71,19 @@ build_flags = src_filter = ${common.src_filter} - + +[env:esp8266] +extends = common:esp8266 +build_flags = ${common:esp8266.build_flags} ${runtime.build_flags} + +[env:esp8266-tidy] +extends = common:esp8266 +build_flags = ${common:esp8266.build_flags} ${clangtidy.build_flags} + +[env:esp32] +extends = common:esp32 +build_flags = ${common:esp32.build_flags} ${runtime.build_flags} + +[env:esp32-tidy] +extends = common:esp32 +build_flags = ${common:esp32.build_flags} ${clangtidy.build_flags} diff --git a/requirements.txt b/requirements.txt index 561bf0f4d5..b4d557f06e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,6 @@ pytz==2021.1 pyserial==3.5 ifaddr==0.1.7 platformio==5.1.1 -esptool==2.8 +esptool==3.1 click==7.1.2 esphome-dashboard==20210728.0 diff --git a/requirements_test.txt b/requirements_test.txt index f9566d5adc..684582bd4c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ -pylint==2.8.2 +pylint==2.9.6 flake8==3.9.2 -black==21.6b0 +black==21.7b0 pexpect==4.8.0 pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 56c3e8ccc8..6983090fd9 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -661,8 +661,12 @@ def build_message_type(desc): o += "\n" o += f" {o2}\n" o += "}\n" + cpp += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n" cpp += o - prot = "void dump_to(std::string &out) const override;" + cpp += f"#endif\n" + prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" + prot += "void dump_to(std::string &out) const override;\n" + prot += "#endif\n" public_content.append(prot) out = f"class {desc.name} : public ProtoMessage {{\n" @@ -774,7 +778,9 @@ def build_service_message_type(mt): hout += f"bool {func}(const {mt.name} &msg);\n" cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n" if log: + cout += f'#ifdef HAS_PROTO_MESSAGE_DUMP\n' cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + cout += f'#endif\n' # cout += f' this->set_nodelay({str(nodelay).lower()});\n' cout += f" return this->send_message_<{mt.name}>(msg, {id_});\n" cout += f"}}\n" @@ -788,7 +794,9 @@ def build_service_message_type(mt): case += f"{mt.name} msg;\n" case += f"msg.decode(msg_data, msg_size);\n" if log: + case += f'#ifdef HAS_PROTO_MESSAGE_DUMP\n' case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + case += f'#endif\n' case += f"this->{func}(msg);\n" if ifdef is not None: case += f"#endif\n" diff --git a/script/build_compile_commands.py b/script/build_compile_commands.py deleted file mode 100755 index 4ac14f08b4..0000000000 --- a/script/build_compile_commands.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -import sys -import os.path - -sys.path.append(os.path.dirname(__file__)) -from helpers import build_all_include, build_compile_commands - - -def main(): - build_all_include() - build_compile_commands() - print("Done.") - - -if __name__ == "__main__": - main() diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 89d621fd5a..1ab0ffa015 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -419,7 +419,7 @@ def get_jschema(path, vschema, create_return_ref=True): def get_schema_str(vschema): - # Hack on cs.use_id, in the future this can be improved by trackign which type is required by + # Hack on cs.use_id, in the future this can be improved by tracking which type is required by # the id, this information can be added somehow to schema (not supported by jsonschema) and # completion can be improved listing valid ids only Meanwhile it's a problem because it makes # all partial schemas with cv.use_id different, e.g. i2c @@ -675,7 +675,7 @@ def dump_schema(): # The root directory of the repo root = Path(__file__).parent.parent - # Fake some diretory so that get_component works + # Fake some directory so that get_component works CORE.config_path = str(root) file_path = args.output diff --git a/script/ci-custom.py b/script/ci-custom.py index d79e5b5e2f..5dad3e2445 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -563,6 +563,7 @@ def lint_inclusive_language(fname, match): "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", "esphome/components/nextion/nextion_base.h", + "esphome/components/select/select.h", "esphome/components/sensor/sensor.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", diff --git a/script/clang-format b/script/clang-format index bb2b722e1c..d6588f1ccb 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse import multiprocessing import os +import queue import re import subprocess import sys @@ -13,59 +12,47 @@ import threading import click sys.path.append(os.path.dirname(__file__)) -from helpers import basepath, get_output, git_ls_files, filter_changed - -is_py2 = sys.version[0] == '2' - -if is_py2: - import Queue as queue -else: - import queue as queue - -root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..'))) -basepath = os.path.join(root_path, 'esphome') -rel_basepath = os.path.relpath(basepath, os.getcwd()) +from helpers import get_output, git_ls_files, filter_changed -def run_format(args, queue, lock): - """Takes filenames out of queue and runs clang-tidy on them.""" +def run_format(args, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() invocation = ['clang-format-11'] if args.inplace: invocation.append('-i') + else: + invocation.extend(['--dry-run', '-Werror']) invocation.append(path) - proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, err = proc.communicate() - with lock: - if proc.returncode != 0: - print(' '.join(invocation)) - print(output.decode('utf-8')) - print(err.decode('utf-8')) + proc = subprocess.run(invocation, capture_output=True, encoding='utf-8') + if proc.returncode != 0: + with lock: + print() + print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) + print(proc.stdout) + print(proc.stderr) + print() + failed_files.append(path) queue.task_done() def progress_bar_show(value): - if value is None: - return '' - return value + return value if value is not None else '' def main(): parser = argparse.ArgumentParser() parser.add_argument('-j', '--jobs', type=int, default=multiprocessing.cpu_count(), - help='number of tidy instances to be run in parallel.') + help='number of format instances to be run in parallel.') parser.add_argument('files', nargs='*', default=[], help='files to be processed (regex on path)') parser.add_argument('-i', '--inplace', action='store_true', - help='apply fix-its') - parser.add_argument('-q', '--quiet', action='store_false', - help='Run clang-tidy in quiet mode') + help='reformat files in-place') parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') + help='only run on changed files') args = parser.parse_args() try: @@ -75,7 +62,7 @@ def main(): Oops. It looks like clang-format is not installed. Please check you can run "clang-format-11 -version" in your terminal and install - clang-format (v7) if necessary. + clang-format (v11) if necessary. Note you can also upload your code as a pull request on GitHub and see the CI check output to apply clang-format. @@ -83,28 +70,26 @@ def main(): return 1 files = [] - for path in git_ls_files(): - filetypes = ('.cpp', '.h', '.tcc') - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, os.getcwd()) - files.append(path) - # Match against re - file_name_re = re.compile('|'.join(args.files)) - files = [p for p in files if file_name_re.search(p)] + for path in git_ls_files(['*.cpp', '*.h', '*.tcc']): + files.append(os.path.relpath(path, os.getcwd())) + + if args.files: + # Match against files specified on command-line + file_name_re = re.compile('|'.join(args.files)) + files = [p for p in files if file_name_re.search(p)] if args.changed: files = filter_changed(files) files.sort() - return_code = 0 + failed_files = [] try: task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread(target=run_format, - args=(args, task_queue, lock)) + args=(args, task_queue, lock, failed_files)) t.daemon = True t.start() @@ -122,7 +107,7 @@ def main(): print('Ctrl-C detected, goodbye.') os.kill(0, 9) - sys.exit(return_code) + sys.exit(len(failed_files)) if __name__ == '__main__': diff --git a/script/clang-tidy b/script/clang-tidy index 0bf17f9076..c2f47bad11 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse +import json import multiprocessing import os +import queue import re import shutil import subprocess @@ -16,41 +16,64 @@ import click import pexpect sys.path.append(os.path.dirname(__file__)) -from helpers import basepath, shlex_quote, get_output, build_compile_commands, \ - build_all_include, temp_header_file, git_ls_files, filter_changed - -is_py2 = sys.version[0] == '2' - -if is_py2: - import Queue as queue -else: - import queue as queue +from helpers import shlex_quote, get_output, \ + build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata -def run_tidy(args, tmpdir, queue, lock, failed_files): +def clang_options(idedata): + cmd = [ + # target 32-bit arch (this prevents size mismatch errors on a 64-bit host) + '-m32', + # disable built-in include directories from the host + '-nostdinc', + '-nostdinc++', + # allow to condition code on the presence of clang-tidy + '-DCLANG_TIDY' + ] + + # copy compiler flags, except those clang doesn't understand. + cmd.extend(flag for flag in idedata['cxx_flags'].split(' ') + if flag not in ('-free', '-fipa-pta', '-mlongcalls', '-mtext-section-literals')) + + # defines + cmd.extend(f'-D{define}' for define in idedata['defines']) + + # add include directories, using -isystem for dependencies to suppress their errors + for directory in idedata['includes']['toolchain']: + cmd.extend(['-isystem', directory]) + for directory in sorted(set(idedata['includes']['build'])): + dependency = "framework-arduino" in directory or "/libdeps/" in directory + cmd.extend(['-isystem' if dependency else '-I', directory]) + + return cmd + + +def run_tidy(args, options, tmpdir, queue, lock, failed_files): while True: path = queue.get() - invocation = ['clang-tidy-11', '-header-filter=^{}/.*'.format(re.escape(basepath))] + invocation = ['clang-tidy-11'] + if tmpdir is not None: - invocation.append('-export-fixes') + invocation.append('--export-fixes') # Get a temporary file. We immediately close the handle so clang-tidy can # overwrite it. (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) os.close(handle) invocation.append(name) - invocation.append('-p=.') + if args.quiet: invocation.append('-quiet') - for arg in ['-Wfor-loop-analysis', '-Wshadow-field', '-Wshadow-field-in-constructor']: - invocation.append('-extra-arg={}'.format(arg)) + invocation.append(os.path.abspath(path)) + invocation.append('--') + invocation.extend(options) invocation_s = ' '.join(shlex_quote(x) for x in invocation) # Use pexpect for a pseudy-TTY with colored output output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8', timeout=15 * 60) - with lock: - if rc != 0: + if rc != 0: + with lock: print() print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) print(output) @@ -78,15 +101,15 @@ def main(): help='files to be processed (regex on path)') parser.add_argument('--fix', action='store_true', help='apply fix-its') parser.add_argument('-q', '--quiet', action='store_false', - help='Run clang-tidy in quiet mode') + help='run clang-tidy in quiet mode') parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') - parser.add_argument('--split-num', type=int, help='Split the files into X jobs.', + help='only run on changed files') + parser.add_argument('--split-num', type=int, help='split the files into X jobs.', default=None) - parser.add_argument('--split-at', type=int, help='Which split is this? Starts at 1', + parser.add_argument('--split-at', type=int, help='which split is this? starts at 1', default=None) parser.add_argument('--all-headers', action='store_true', - help='Create a dummy file that checks all headers') + help='create a dummy file that checks all headers') args = parser.parse_args() try: @@ -103,19 +126,17 @@ def main(): """) return 1 - build_all_include() - build_compile_commands() + idedata = load_idedata("esp8266-tidy") + options = clang_options(idedata) files = [] - for path in git_ls_files(): - filetypes = ('.cpp',) - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, os.getcwd()) - files.append(path) - # Match against re - file_name_re = re.compile('|'.join(args.files)) - files = [p for p in files if file_name_re.search(p)] + for path in git_ls_files(['*.cpp']): + files.append(os.path.relpath(path, os.getcwd())) + + if args.files: + # Match against files specified on command-line + file_name_re = re.compile('|'.join(args.files)) + files = [p for p in files if file_name_re.search(p)] if args.changed: files = filter_changed(files) @@ -126,6 +147,7 @@ def main(): files = split_list(files, args.split_num)[args.split_at - 1] if args.all_headers and args.split_at in (None, 1): + build_all_include() files.insert(0, temp_header_file) tmpdir = None @@ -133,13 +155,12 @@ def main(): tmpdir = tempfile.mkdtemp() failed_files = [] - return_code = 0 try: task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread(target=run_tidy, - args=(args, tmpdir, task_queue, lock, failed_files)) + args=(args, options, tmpdir, task_queue, lock, failed_files)) t.daemon = True t.start() @@ -151,7 +172,6 @@ def main(): # Wait for all threads to be done. task_queue.join() - return_code = len(failed_files) except KeyboardInterrupt: print() @@ -168,8 +188,8 @@ def main(): print('Error applying fixes.\n', file=sys.stderr) raise - return return_code + sys.exit(len(failed_files)) if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/script/helpers.py b/script/helpers.py index 1a4402aa1d..7200078822 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,13 +1,14 @@ import codecs -import json import os.path import re import subprocess -import sys +import json +from pathlib import Path root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") -temp_header_file = os.path.join(root_path, ".temp-clang-tidy.cpp") +temp_folder = os.path.join(root_path, ".temp") +temp_header_file = os.path.join(temp_folder, "all-include.cpp") def shlex_quote(s): @@ -33,63 +34,9 @@ def build_all_include(): headers.sort() headers.append("") content = "\n".join(headers) - with codecs.open(temp_header_file, "w", encoding="utf-8") as f: - f.write(content) - - -def build_compile_commands(): - gcc_flags_json = os.path.join(root_path, ".gcc-flags.json") - if not os.path.isfile(gcc_flags_json): - print("Could not find {} file which is required for clang-tidy.".format(gcc_flags_json)) - print( - 'Please run "pio init --ide atom" in the root esphome folder to generate that file.' - ) - sys.exit(1) - with codecs.open(gcc_flags_json, "r", encoding="utf-8") as f: - gcc_flags = json.load(f) - exec_path = gcc_flags["execPath"] - include_paths = gcc_flags["gccIncludePaths"].split(",") - includes = [f"-I{p}" for p in include_paths] - cpp_flags = gcc_flags["gccDefaultCppFlags"].split(" ") - defines = [flag for flag in cpp_flags if flag.startswith("-D")] - command = [exec_path] - command.extend(includes) - command.extend(defines) - command.append("-std=gnu++11") - command.append("-Wall") - command.append("-Wno-delete-non-virtual-dtor") - command.append("-Wno-unused-variable") - command.append("-Wunreachable-code") - - source_files = [] - for path in walk_files(basepath): - filetypes = (".cpp",) - ext = os.path.splitext(path)[1] - if ext in filetypes: - source_files.append(os.path.abspath(path)) - source_files.append(temp_header_file) - source_files.sort() - compile_commands = [ - { - "directory": root_path, - "command": " ".join( - shlex_quote(x) for x in (command + ["-o", p + ".o", "-c", p]) - ), - "file": p, - } - for p in source_files - ] - compile_commands_json = os.path.join(root_path, "compile_commands.json") - if os.path.isfile(compile_commands_json): - with codecs.open(compile_commands_json, "r", encoding="utf-8") as f: - try: - if json.load(f) == compile_commands: - return - # pylint: disable=bare-except - except: - pass - with codecs.open(compile_commands_json, "w", encoding="utf-8") as f: - json.dump(compile_commands, f, indent=2) + p = Path(temp_header_file) + p.parent.mkdir(exist_ok=True) + p.write_text(content) def walk_files(path): @@ -145,9 +92,33 @@ def filter_changed(files): return files -def git_ls_files(): +def git_ls_files(patterns=None): command = ["git", "ls-files", "-s"] + if patterns is not None: + command.extend(patterns) proc = subprocess.Popen(command, stdout=subprocess.PIPE) output, err = proc.communicate() lines = [x.split() for x in output.decode("utf-8").splitlines()] return {s[3].strip(): int(s[0]) for s in lines} + + +def load_idedata(environment): + platformio_ini = Path(root_path) / "platformio.ini" + temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" + if not platformio_ini.is_file() or not temp_idedata.is_file(): + changed = True + elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + changed = True + else: + changed = False + + if not changed: + return json.loads(temp_idedata.read_text()) + + stdout = subprocess.check_output(["pio", "run", "-t", "idedata", "-e", environment]) + match = re.search(r'{\s*".*}', stdout.decode("utf-8")) + data = json.loads(match.group()) + + temp_idedata.parent.mkdir(exist_ok=True) + temp_idedata.write_text(json.dumps(data, indent=2) + "\n") + return data diff --git a/tests/test1.yaml b/tests/test1.yaml index f9fe7d106d..bcf5f932a8 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -540,6 +540,11 @@ sensor: sensor: hlw8012_power name: 'Integration Sensor' time_unit: s + - platform: integration + sensor: hlw8012_power + name: 'Integration Sensor lazy' + time_unit: s + min_save_interval: 60s - platform: hmc5883l address: 0x68 field_strength_x: @@ -670,6 +675,27 @@ sensor: name: 'Outside Pressure' address: 0x77 update_interval: 15s + - platform: pmsa003i + pm_1_0: + name: "PMSA003i PM1.0" + pm_2_5: + name: "PMSA003i PM2.5" + pm_10_0: + name: "PMSA003i PM10.0" + pmc_0_3: + name: "PMSA003i PMC <0.3µm" + pmc_0_5: + name: "PMSA003i PMC <0.5µm" + pmc_1_0: + name: "PMSA003i PMC <1µm" + pmc_2_5: + name: "PMSA003i PMC <2.5µm" + pmc_5_0: + name: "PMSA003i PMC <5µm" + pmc_10_0: + name: "PMSA003i PMC <10µm" + address: 0x12 + standard_units: True - platform: pulse_counter name: 'Pulse Counter' pin: GPIO12 @@ -835,6 +861,25 @@ sensor: is_cs_package: true integration_time: 402ms gain: 16x + - platform: tsl2591 + id: this_little_light_of_mine + address: 0x29 + update_interval: 15s + integration_time: 600ms + gain: high + visible: + name: "tsl2591 visible" + id: tsl2591_vis + unit_of_measurement: 'pH' + infrared: + name: "tsl2591 infrared" + id: tsl2591_ir + full_spectrum: + name: "tsl2591 full_spectrum" + id: tsl2591_fs + calculated_lux: + name: "tsl2591 calculated_lux" + id: tsl2591_cl - platform: ultrasonic trigger_pin: GPIO25 echo_pin: @@ -922,6 +967,11 @@ sensor: id: ph_ezo address: 99 unit_of_measurement: 'pH' + - platform: sdp3x + name: "HVAC Filter Pressure drop" + id: filter_pressure + update_interval: 5s + accuracy_decimals: 3 - platform: cs5460a id: cs5460a1 current: @@ -1379,6 +1429,16 @@ light: cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds color_interlock: true + - platform: rgbct + name: 'Living Room Lights 2' + red: pca_3 + green: pca_4 + blue: pca_5 + color_temperature: pca_6 + white_brightness: pca_6 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds + color_interlock: true - platform: cwww name: 'Living Room Lights 2' cold_white: pca_6 @@ -1386,6 +1446,12 @@ light: cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds constant_brightness: true + - platform: color_temperature + name: 'Living Room Lights 2' + color_temperature: pca_6 + brightness: pca_6 + cold_white_color_temperature: 153 mireds + warm_white_color_temperature: 500 mireds - platform: fastled_clockless id: addr1 chipset: WS2811 @@ -1556,6 +1622,7 @@ climate: - platform: anova name: Anova cooker ble_client_id: ble_blah + unit_of_measurement: c midea_dongle: uart_id: uart0 @@ -1615,6 +1682,12 @@ switch: remote_transmitter.transmit_samsung36: address: 0x0400 command: 0x000E00FF + - platform: template + name: ToshibaAC + turn_on_action: + - remote_transmitter.transmit_toshiba_ac: + rc_code_1: 0xB24DBF4050AF + rc_code_2: 0xD5660001003C - platform: template name: Sony turn_on_action: @@ -2072,6 +2145,10 @@ cover: id: template_cover state: CLOSED assumed_state: no + - platform: am43 + name: 'Test AM43' + id: am43_test + ble_client_id: ble_foo debug: diff --git a/tests/test2.yaml b/tests/test2.yaml index faa76300cc..6807278c0d 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -218,6 +218,16 @@ sensor: name: 'ATC Battery-Level' battery_voltage: name: 'ATC Battery-Voltage' + - platform: pvvx_mithermometer + mac_address: 'A4:C1:38:4E:16:78' + temperature: + name: 'PVVX Temperature' + humidity: + name: 'PVVX Humidity' + battery_level: + name: 'PVVX Battery-Level' + battery_voltage: + name: 'PVVX Battery-Voltage' - platform: inkbird_ibsth1_mini mac_address: 38:81:D7:0A:9C:11 temperature: @@ -293,6 +303,16 @@ binary_sensor: name: 'WX08ZM Tablet Resource' battery_level: name: 'WX08ZM Battery Level' + - platform: xiaomi_cgpr1 + name: 'CGPR1 Motion' + mac_address: '12:34:56:12:34:56' + bindkey: '48403ebe2d385db8d0c187f81e62cb64' + battery_level: + name: 'CGPR1 battery Level' + idle_time: + name: 'CGPR1 Idle Time' + illuminance: + name: 'CGPR1 Illuminance' esp32_ble_tracker: on_ble_advertise: diff --git a/tests/test3.yaml b/tests/test3.yaml index acd975d794..e35c1e611c 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -450,6 +450,66 @@ sensor: name: 'PM 2.5 Concentration' pm_10_0: name: 'PM 10.0 Concentration' + pm_1_0_std: + name: 'PM 1.0 Standard Atmospher Concentration' + pm_2_5_std: + name: 'PM 2.5 Standard Atmospher Concentration' + pm_10_0_std: + name: 'PM 10.0 Standard Atmospher Concentration' + pm_0_3um: + name: 'Particulate Count >0.3um' + pm_0_5um: + name: 'Particulate Count >0.5um' + pm_1_0um: + name: 'Particulate Count >1.0um' + pm_2_5um: + name: 'Particulate Count >2.5um' + pm_5_0um: + name: 'Particulate Count >5.0um' + pm_10_0um: + name: 'Particulate Count >10.0um' + - platform: pmsx003 + uart_id: uart2 + type: PMS5003T + pm_2_5: + name: 'PM 2.5 Concentration' + temperature: + name: 'PMS Temperature' + humidity: + name: 'PMS Humidity' + - platform: pmsx003 + uart_id: uart2 + type: PMS5003ST + pm_1_0: + name: 'PM 1.0 Concentration' + pm_2_5: + name: 'PM 2.5 Concentration' + pm_10_0: + name: 'PM 10.0 Concentration' + pm_1_0_std: + name: 'PM 1.0 Standard Atmospher Concentration' + pm_2_5_std: + name: 'PM 2.5 Standard Atmospher Concentration' + pm_10_0_std: + name: 'PM 10.0 Standard Atmospher Concentration' + pm_0_3um: + name: 'Particulate Count >0.3um' + pm_0_5um: + name: 'Particulate Count >0.5um' + pm_1_0um: + name: 'Particulate Count >1.0um' + pm_2_5um: + name: 'Particulate Count >2.5um' + pm_5_0um: + name: 'Particulate Count >5.0um' + pm_10_0um: + name: 'Particulate Count >10.0um' + temperature: + name: 'PMS Temperature' + humidity: + name: 'PMS Humidity' + formaldehyde: + name: 'PMS Formaldehyde Concentration' - platform: cse7766 uart_id: uart3 voltage: @@ -534,6 +594,9 @@ sensor: name: 'Import Reactive Energy' export_reactive_energy: name: 'Export Reactive Energy' + - platform: dsmr + energy_delivered_tariff1: + name: dsmr_energy_delivered_tariff1 - platform: nextion id: testnumber @@ -675,6 +738,11 @@ text_sensor: id: text0 update_interval: 4s component_name: text0 + - platform: dsmr + identification: + name: "dsmr_identification" + p1_version: + name: "dsmr_p1_version" script: - id: my_script @@ -798,8 +866,12 @@ climate: - switch.turn_on: gpio_switch1 cool_action: - switch.turn_on: gpio_switch2 + supplemental_cooling_action: + - switch.turn_on: gpio_switch3 heat_action: - switch.turn_on: gpio_switch1 + supplemental_heating_action: + - switch.turn_on: gpio_switch3 dry_action: - switch.turn_on: gpio_switch2 fan_only_action: @@ -842,7 +914,28 @@ climate: - switch.turn_on: gpio_switch1 swing_both_action: - switch.turn_on: gpio_switch2 - hysteresis: 0.2 + startup_delay: true + supplemental_cooling_delta: 2.0 + cool_deadband: 0.5 + cool_overrun: 0.5 + min_cooling_off_time: 300s + min_cooling_run_time: 300s + max_cooling_run_time: 600s + supplemental_heating_delta: 2.0 + heat_deadband: 0.5 + heat_overrun: 0.5 + min_heating_off_time: 300s + min_heating_run_time: 300s + max_heating_run_time: 600s + min_fanning_off_time: 30s + min_fanning_run_time: 30s + min_fan_mode_switching_time: 15s + min_idle_time: 30s + set_point_minimum_differential: 0.5 + fan_only_action_uses_fan_mode_timer: true + fan_only_cooling: true + fan_with_cooling: true + fan_with_heating: true away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C @@ -1157,3 +1250,7 @@ fingerprint_grow: data: finger_id: !lambda 'return finger_id;' uart_id: uart6 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + uart_id: uart6 diff --git a/tests/test4.yaml b/tests/test4.yaml index 7868fd4968..e52e8cc33c 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -56,6 +56,9 @@ time: tuya: time_id: sntp_time +pipsolar: + id: inverter0 + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -63,6 +66,149 @@ sensor: - platform: tuya id: tuya_sensor sensor_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + grid_rating_voltage: + id: inverter0_grid_rating_voltage + name: inverter0_grid_rating_voltage + grid_rating_current: + id: inverter0_grid_rating_current + name: inverter0_grid_rating_current + ac_output_rating_voltage: + id: inverter0_ac_output_rating_voltage + name: inverter0_ac_output_rating_voltage + ac_output_rating_frequency: + id: inverter0_ac_output_rating_frequency + name: inverter0_ac_output_rating_frequency + ac_output_rating_current: + id: inverter0_ac_output_rating_current + name: inverter0_ac_output_rating_current + ac_output_rating_apparent_power: + id: inverter0_ac_output_rating_apparent_power + name: inverter0_ac_output_rating_apparent_power + ac_output_rating_active_power: + id: inverter0_ac_output_rating_active_power + name: inverter0_ac_output_rating_active_power + battery_rating_voltage: + id: inverter0_battery_rating_voltage + name: inverter0_battery_rating_voltage + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage + name: inverter0_battery_recharge_voltage + battery_under_voltage: + id: inverter0_battery_under_voltage + name: inverter0_battery_under_voltage + battery_bulk_voltage: + id: inverter0_battery_bulk_voltage + name: inverter0_battery_bulk_voltage + battery_float_voltage: + id: inverter0_battery_float_voltage + name: inverter0_battery_float_voltage + battery_type: + id: inverter0_battery_type + name: inverter0_battery_type + current_max_ac_charging_current: + id: inverter0_current_max_ac_charging_current + name: inverter0_current_max_ac_charging_current + current_max_charging_current: + id: inverter0_current_max_charging_current + name: inverter0_current_max_charging_current + input_voltage_range: + id: inverter0_input_voltage_range + name: inverter0_input_voltage_range + output_source_priority: + id: inverter0_output_source_priority + name: inverter0_output_source_priority + charger_source_priority: + id: inverter0_charger_source_priority + name: inverter0_charger_source_priority + parallel_max_num: + id: inverter0_parallel_max_num + name: inverter0_parallel_max_num + machine_type: + id: inverter0_machine_type + name: inverter0_machine_type + topology: + id: inverter0_topology + name: inverter0_topology + output_mode: + id: inverter0_output_mode + name: inverter0_output_mode + battery_redischarge_voltage: + id: inverter0_battery_redischarge_voltage + name: inverter0_battery_redischarge_voltage + pv_ok_condition_for_parallel: + id: inverter0_pv_ok_condition_for_parallel + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + id: inverter0_pv_power_balance + name: inverter0_pv_power_balance + grid_voltage: + id: inverter0_grid_voltage + name: inverter0_grid_voltage + grid_frequency: + id: inverter0_grid_frequency + name: inverter0_grid_frequency + ac_output_voltage: + id: inverter0_ac_output_voltage + name: inverter0_ac_output_voltage + ac_output_frequency: + id: inverter0_ac_output_frequency + name: inverter0_ac_output_frequency + ac_output_apparent_power: + id: inverter0_ac_output_apparent_power + name: inverter0_ac_output_apparent_power + ac_output_active_power: + id: inverter0_ac_output_active_power + name: inverter0_ac_output_active_power + output_load_percent: + id: inverter0_output_load_percent + name: inverter0_output_load_percent + bus_voltage: + id: inverter0_bus_voltage + name: inverter0_bus_voltage + battery_voltage: + id: inverter0_battery_voltage + name: inverter0_battery_voltage + battery_charging_current: + id: inverter0_battery_charging_current + name: inverter0_battery_charging_current + battery_capacity_percent: + id: inverter0_battery_capacity_percent + name: inverter0_battery_capacity_percent + inverter_heat_sink_temperature: + id: inverter0_inverter_heat_sink_temperature + name: inverter0_inverter_heat_sink_temperature + pv_input_current_for_battery: + id: inverter0_pv_input_current_for_battery + name: inverter0_pv_input_current_for_battery + pv_input_voltage: + id: inverter0_pv_input_voltage + name: inverter0_pv_input_voltage + battery_voltage_scc: + id: inverter0_battery_voltage_scc + name: inverter0_battery_voltage_scc + battery_discharge_current: + id: inverter0_battery_discharge_current + name: inverter0_battery_discharge_current + battery_voltage_offset_for_fans_on: + id: inverter0_battery_voltage_offset_for_fans_on + name: inverter0_battery_voltage_offset_for_fans_on + eeprom_version: + id: inverter0_eeprom_version + name: inverter0_eeprom_version + pv_charging_power: + id: inverter0_pv_charging_power + name: inverter0_pv_charging_power + - platform: "hrxl_maxsonar_wr" + name: "Rainwater Tank Level" + filters: + - sliding_window_moving_average: + window_size: 12 + send_every: 12 + - or: + - throttle: "20min" + - delta: 0.02 # # platform sensor.apds9960 requires component apds9960 # @@ -86,6 +232,59 @@ binary_sensor: - platform: tuya id: tuya_binary_sensor sensor_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + add_sbu_priority_version: + id: inverter0_add_sbu_priority_version + name: inverter0_add_sbu_priority_version + configuration_status: + id: inverter0_configuration_status + name: inverter0_configuration_status + scc_firmware_version: + id: inverter0_scc_firmware_version + name: inverter0_scc_firmware_version + load_status: + id: inverter0_load_status + name: inverter0_load_status + battery_voltage_to_steady_while_charging: + id: inverter0_battery_voltage_to_steady_while_charging + name: inverter0_battery_voltage_to_steady_while_charging + charging_status: + id: inverter0_charging_status + name: inverter0_charging_status + scc_charging_status: + id: inverter0_scc_charging_status + name: inverter0_scc_charging_status + ac_charging_status: + id: inverter0_ac_charging_status + name: inverter0_ac_charging_status + charging_to_floating_mode: + id: inverter0_charging_to_floating_mode + name: inverter0_charging_to_floating_mode + switch_on: + id: inverter0_switch_on + name: inverter0_switch_on + dustproof_installed: + id: inverter0_dustproof_installed + name: inverter0_dustproof_installed + silence_buzzer_open_buzzer: + id: inverter0_silence_buzzer_open_buzzer + name: inverter0_silence_buzzer_open_buzzer + overload_bypass_function: + id: inverter0_overload_bypass_function + name: inverter0_overload_bypass_function + lcd_escape_to_default: + id: inverter0_lcd_escape_to_default + name: inverter0_lcd_escape_to_default + overload_restart_function: + id: inverter0_overload_restart_function + name: inverter0_overload_restart_function + over_temperature_restart_function: + id: inverter0_over_temperature_restart_function + name: inverter0_over_temperature_restart_function + backlight_on: + id: inverter0_backlight_on + name: inverter0_backlight_on - platform: template id: ar1 lambda: 'return {};' @@ -122,6 +321,20 @@ switch: - platform: tuya id: tuya_switch switch_datapoint: 1 + - platform: pipsolar + pipsolar_id: inverter0 + output_source_priority_utility: + name: inverter0_output_source_priority_utility + output_source_priority_solar: + name: inverter0_output_source_priority_solar + output_source_priority_battery: + name: inverter0_output_source_priority_battery + input_voltage_range: + name: inverter0_input_voltage_range + pv_ok_condition_for_parallel: + name: inverter0_pv_ok_condition_for_parallel + pv_power_balance: + name: inverter0_pv_power_balance light: - platform: fastled_clockless @@ -195,6 +408,30 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +text_sensor: + - platform: pipsolar + pipsolar_id: inverter0 + device_mode: + id: inverter0_device_mode + name: inverter0_device_mode + last_qpigs: + id: inverter0_last_qpigs + name: inverter0_last_qpigs + last_qpiri: + id: inverter0_last_qpiri + name: inverter0_last_qpiri + last_qmod: + id: inverter0_last_qmod + name: inverter0_last_qmod + last_qflag: + id: inverter0_last_qflag + name: inverter0_last_qflag + +output: + - platform: pipsolar + pipsolar_id: inverter0 + battery_recharge_voltage: + id: inverter0_battery_recharge_voltage_out esp32_camera: name: ESP-32 Camera data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19] diff --git a/tests/test5.yaml b/tests/test5.yaml index 35225402a3..5d4ff025d9 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -18,16 +18,42 @@ ota: logger: +uart: + - id: uart1 + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 + - id: uart2 + tx_pin: 17 + rx_pin: 16 + baud_rate: 19200 + + +modbus: + uart_id: uart1 + binary_sensor: - platform: gpio pin: GPIO0 id: io0_button +tlc5947: + data_pin: GPIO12 + clock_pin: GPIO14 + lat_pin: GPIO15 + output: - platform: gpio pin: GPIO2 id: built_in_led + - platform: tlc5947 + id: output_red + channel: 0 + max_power: 0.8 + +demo: + esp32_ble: esp32_ble_server: @@ -55,3 +81,71 @@ number: max_value: 100 min_value: 0 step: 5 + +select: + - platform: template + name: My template select + id: template_select_id + optimistic: true + initial_option: two + restore_value: true + on_value: + - logger.log: + format: "Select changed to %s" + args: ["x.c_str()"] + set_action: + - logger.log: + format: "Template Select set to %s" + args: ["x.c_str()"] + - select.set: + id: template_select_id + option: two + options: + - one + - two + - three + +sensor: + - platform: selec_meter + total_active_energy: + name: "SelecEM2M Total Active Energy" + import_active_energy: + name: "SelecEM2M Import Active Energy" + export_active_energy: + name: "SelecEM2M Export Active Energy" + total_reactive_energy: + name: "SelecEM2M Total Reactive Energy" + import_reactive_energy: + name: "SelecEM2M Import Reactive Energy" + export_reactive_energy: + name: "SelecEM2M Export Reactive Energy" + apparent_energy: + name: "SelecEM2M Apparent Energy" + active_power: + name: "SelecEM2M Active Power" + reactive_power: + name: "SelecEM2M Reactive Power" + apparent_power: + name: "SelecEM2M Apparent Power" + voltage: + name: "SelecEM2M Voltage" + current: + name: "SelecEM2M Current" + power_factor: + name: "SelecEM2M Power Factor" + frequency: + name: "SelecEM2M Frequency" + maximum_demand_active_power: + name: "SelecEM2M Maximum Demand Active Power" + disabled_by_default: true + maximum_demand_reactive_power: + name: "SelecEM2M Maximum Demand Reactive Power" + disabled_by_default: true + maximum_demand_apparent_power: + name: "SelecEM2M Maximum Demand Apparent Power" + disabled_by_default: true + + - platform: t6615 + uart_id: uart2 + co2: + name: CO2 Sensor diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 16cfb16e94..e34c7064fa 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -40,6 +40,28 @@ def test_valid_name__invalid(value): config_validation.valid_name(value) +@pytest.mark.parametrize("value", ("foo", "bar123", "foo-bar")) +def test_hostname__valid(value): + actual = config_validation.hostname(value) + + assert actual == value + + +@pytest.mark.parametrize("value", ("foo bar", "foobar ", "foo#bar")) +def test_hostname__invalid(value): + with pytest.raises(Invalid): + config_validation.hostname(value) + + +def test_hostname__warning(caplog): + actual = config_validation.hostname("foo_bar") + assert actual == "foo_bar" + assert ( + "Using the '_' (underscore) character in the hostname is discouraged" + in caplog.text + ) + + @given(one_of(integers(), text())) def test_string__valid(value): actual = config_validation.string(value) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 4e60880033..9e4ad3d79d 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -444,16 +444,24 @@ class TestDefine: class TestLibrary: @pytest.mark.parametrize( - "name, value, prop, expected", + "name, version, repository, prop, expected", ( - ("mylib", None, "as_lib_dep", "mylib"), - ("mylib", None, "as_tuple", ("mylib", None)), - ("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"), - ("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")), + ("mylib", None, None, "as_lib_dep", "mylib"), + ("mylib", None, None, "as_tuple", ("mylib", None, None)), + ("mylib", "1.2.3", None, "as_lib_dep", "mylib@1.2.3"), + ("mylib", "1.2.3", None, "as_tuple", ("mylib", "1.2.3", None)), + ("mylib", None, "file:///test", "as_lib_dep", "mylib=file:///test"), + ( + "mylib", + None, + "file:///test", + "as_tuple", + ("mylib", None, "file:///test"), + ), ), ) - def test_properties(self, name, value, prop, expected): - target = core.Library(name, value) + def test_properties(self, name, version, repository, prop, expected): + target = core.Library(name, version, repository) actual = getattr(target, prop) @@ -465,6 +473,7 @@ class TestLibrary: ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), ("__eq__", core.Library(name="libbar", version="1.2.3"), False), + ("__eq__", core.Library(name="libbar", version=None, repository="file:///test"), False), ("__eq__", 1000, NotImplemented), ("__eq__", "1000", NotImplemented), ("__eq__", True, NotImplemented), diff --git a/tests/unit_tests/test_pins.py b/tests/unit_tests/test_pins.py index 6bc6f4d766..d2ffd5f7cd 100644 --- a/tests/unit_tests/test_pins.py +++ b/tests/unit_tests/test_pins.py @@ -11,13 +11,13 @@ import pytest from esphome.config_validation import Invalid from esphome.core import EsphomeCore -from esphome import pins +from esphome import boards, pins MOCK_ESP8266_BOARD_ID = "_mock_esp8266" MOCK_ESP8266_PINS = {"X0": 16, "X1": 5, "X2": 4, "LED": 2} MOCK_ESP8266_BOARD_ALIAS_ID = "_mock_esp8266_alias" -MOCK_ESP8266_FLASH_SIZE = pins.FLASH_SIZE_2_MB +MOCK_ESP8266_FLASH_SIZE = boards.FLASH_SIZE_2_MB MOCK_ESP32_BOARD_ID = "_mock_esp32" MOCK_ESP32_PINS = {"Y0": 12, "Y1": 8, "Y2": 3, "LED": 9, "A0": 8} @@ -31,19 +31,19 @@ def mock_mcu(monkeypatch): """ Add a mock MCU into the lists as a stable fixture """ - pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS - pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE - pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID - pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE - pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS - pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID + boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS + boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE + boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID + boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE + boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS + boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID yield - del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] - del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] - del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] - del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] - del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] - del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] + del boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] + del boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] + del boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] + del boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] + del boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] + del boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] @pytest.fixture diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 0ca7c83e1b..56bd5119b5 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -2,7 +2,7 @@ import esphome.wizard as wz import pytest -from esphome.pins import ESP8266_BOARD_PINS +from esphome.boards import ESP8266_BOARD_PINS from mock import MagicMock