diff --git a/CODEOWNERS b/CODEOWNERS index 1298d4d43d..8aa96d14af 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/airthings_ble/* @jeromelaban +esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/animation/* @syndlex @@ -29,6 +31,7 @@ esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter +esphome/components/ccs811/* @habbie esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz @@ -52,6 +55,8 @@ esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/havells_solar/* @sourabhjaiswal +esphome/components/hbridge/fan/* @WeekendWarrior +esphome/components/hbridge/light/* @DotNetDann esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/hrxl_maxsonar_wr/* @netmikey @@ -75,8 +80,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp9808/* @k7hpn -esphome/components/midea_ac/* @dudanov -esphome/components/midea_dongle/* @dudanov +esphome/components/midea/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw @@ -90,6 +94,7 @@ esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 +esphome/components/pm1006/* @habbie esphome/components/pmsa003i/* @sjtrny esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz @@ -115,6 +120,7 @@ esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/socket/* @esphome/core esphome/components/spi/* @esphome/core esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 @@ -129,6 +135,7 @@ esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_spi/* @kbx81 esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 +esphome/components/st7920/* @marsjan155 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core diff --git a/esphome/boards.py b/esphome/boards.py index 220d440a37..ba6fe889ea 100644 --- a/esphome/boards.py +++ b/esphome/boards.py @@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = { "espectro": {"LED": 15, "BUTTON": 2}, "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, "espinotee": {"LED": 16}, + "espmxdevkit": {}, "espresso_lite_v1": {"LED": 16}, "espresso_lite_v2": {"LED": 2}, "gen4iod": {}, @@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = { }, "phoenix_v1": {"LED": 16}, "phoenix_v2": {"LED": 2}, + "sonoff_basic": {}, + "sonoff_s20": {}, + "sonoff_sv": {}, + "sonoff_th": {}, "sparkfunBlynk": "thing", "thing": {"LED": 5, "SDA": 2, "SCL": 14}, "thingdev": "thing", @@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = { "espectro": FLASH_SIZE_4_MB, "espino": FLASH_SIZE_4_MB, "espinotee": FLASH_SIZE_4_MB, + "espmxdevkit": FLASH_SIZE_1_MB, "espresso_lite_v1": FLASH_SIZE_4_MB, "espresso_lite_v2": FLASH_SIZE_4_MB, "gen4iod": FLASH_SIZE_512_KB, @@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = { "oak": FLASH_SIZE_4_MB, "phoenix_v1": FLASH_SIZE_4_MB, "phoenix_v2": FLASH_SIZE_4_MB, + "sonoff_basic": FLASH_SIZE_1_MB, + "sonoff_s20": FLASH_SIZE_1_MB, + "sonoff_sv": FLASH_SIZE_1_MB, + "sonoff_th": FLASH_SIZE_1_MB, "sparkfunBlynk": FLASH_SIZE_4_MB, "thing": FLASH_SIZE_512_KB, "thingdev": FLASH_SIZE_512_KB, @@ -291,6 +301,7 @@ ESP32_BOARD_PINS = { "SW2": 2, "SW3": 0, }, + "az-delivery-devkit-v4": {}, "bpi-bit": { "BUTTON_A": 35, "BUTTON_B": 27, @@ -320,6 +331,8 @@ ESP32_BOARD_PINS = { "RGB_LED": 4, "TEMPERATURE_SENSOR": 34, }, + "briki_abc_esp32": {}, + "briki_mbc-wb_esp32": {}, "d-duino-32": { "D1": 5, "D10": 1, @@ -380,11 +393,58 @@ ESP32_BOARD_PINS = { "esp32cam": {}, "esp32dev": {}, "esp32doit-devkit-v1": {"LED": 2}, + "esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8}, "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, + "esp32thing_plus": { + "SDA": 23, + "SCL": 22, + "SS": 33, + "MOSI": 18, + "MISO": 19, + "SCK": 5, + "A0": 26, + "A1": 25, + "A2": 34, + "A3": 39, + "A4": 36, + "A5": 4, + "A6": 14, + "A7": 32, + "A8": 15, + "A9": 33, + "A10": 27, + "A11": 12, + "A12": 13, + }, "esp32vn-iot-uno": {}, "espea32": {"BUTTON": 0, "LED": 5}, "espectro32": {"LED": 15, "SD_SS": 33}, "espino32": {"BUTTON": 0, "LED": 16}, + "etboard": { + "LED_BUILTIN": 5, + "TX": 34, + "RX": 35, + "SS": 29, + "MOSI": 37, + "MISO": 31, + "SCK": 30, + "A0": 36, + "A1": 39, + "A2": 32, + "A3": 33, + "A4": 34, + "A5": 35, + "A6": 25, + "A7": 26, + "D2": 27, + "D3": 14, + "D4": 12, + "D5": 13, + "D6": 15, + "D7": 16, + "D8": 17, + "D9": 4, + }, "featheresp32": { "A0": 26, "A1": 25, @@ -434,6 +494,18 @@ ESP32_BOARD_PINS = { "SW4": 21, }, "frogboard": {}, + "healtypi4": { + "KEY_BUILTIN": 17, + "ADS1292_DRDY_PIN": 26, + "ADS1292_CS_PIN": 13, + "ADS1292_START_PIN": 14, + "ADS1292_PWDN_PIN": 27, + "AFE4490_CS_PIN": 21, + "AFE4490_DRDY_PIN": 39, + "AFE4490_PWDN_PIN": 4, + "PUSH_BUTTON": 17, + "SLIDE_SWITCH": 16, + }, "heltec_wifi_kit_32": { "A1": 37, "A2": 38, @@ -444,6 +516,7 @@ ESP32_BOARD_PINS = { "SDA_OLED": 4, "Vext": 21, }, + "heltec_wifi_kit_32_v2": "heltec_wifi_kit_32", "heltec_wifi_lora_32": { "BUTTON": 0, "DIO0": 26, @@ -489,8 +562,68 @@ ESP32_BOARD_PINS = { "SS": 18, "Vext": 21, }, + "heltec_wireless_stick_lite": { + "LED_BUILTIN": 25, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "Vext": 21, + "LED": 25, + "RST_LoRa": 14, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + }, + "honeylemon": { + "LED_BUILTIN": 2, + "BUILTIN_KEY": 0, + }, "hornbill32dev": {"BUTTON": 0, "LED": 13}, "hornbill32minima": {"SS": 2}, + "imbrios-logsens-v1p1": { + "LED_BUILTIN": 33, + "UART2_TX": 17, + "UART2_RX": 16, + "UART2_RTS": 4, + "CAN_TX": 17, + "CAN_RX": 16, + "CAN_TXDE": 4, + "SS": 15, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "SPI_SS1": 23, + "BUZZER_CTRL": 19, + "SD_CARD_DETECT": 35, + "SW2_BUILDIN": 0, + "SW3_BUILDIN": 36, + "SW4_BUILDIN": 34, + "LED1_BUILDIN": 32, + "LED2_BUILDIN": 33, + }, + "inex_openkb": { + "LED_BUILTIN": 16, + "LDR_PIN": 36, + "SW1": 16, + "SW2": 14, + "BT_LED": 17, + "WIFI_LED": 2, + "NTP_LED": 15, + "IOT_LED": 12, + "BUZZER": 13, + "INPUT1": 32, + "INPUT2": 33, + "INPUT3": 34, + "INPUT4": 35, + "OUTPUT1": 26, + "OUTPUT2": 27, + "SDA0": 21, + "SCL0": 22, + "SDA1": 4, + "SCL1": 5, + }, "intorobot": { "A1": 39, "A2": 35, @@ -528,6 +661,40 @@ ESP32_BOARD_PINS = { "iotaap_magnolia": {}, "iotbusio": {}, "iotbusproteus": {}, + "kits-edu": {}, + "labplus_mpython": { + "SDA": 23, + "SCL": 22, + "P0": 33, + "P1": 32, + "P2": 35, + "P3": 34, + "P4": 39, + "P5": 0, + "P6": 16, + "P7": 17, + "P8": 26, + "P9": 25, + "P10": 36, + "P11": 2, + "P13": 18, + "P14": 19, + "P15": 21, + "P16": 5, + "P19": 22, + "P20": 23, + "P": 27, + "Y": 14, + "T": 12, + "H": 13, + "O": 15, + "N": 4, + "BTN_A": 0, + "BTN_B": 2, + "SOUND": 36, + "LIGHT": 39, + "BUZZER": 16, + }, "lolin32": {"LED": 5}, "lolin32_lite": {"LED": 22}, "lolin_d32": {"LED": 5, "_VBAT": 35}, @@ -554,6 +721,16 @@ ESP32_BOARD_PINS = { "SDA": 12, "SS": 18, }, + "m5stack-atom": { + "SDA": 26, + "SCL": 32, + "ADC1": 35, + "ADC2": 36, + "SS": 19, + "MOSI": 33, + "MISO": 23, + "SCK": 22, + }, "m5stack-core-esp32": { "ADC1": 35, "ADC2": 36, @@ -580,6 +757,26 @@ ESP32_BOARD_PINS = { "RXD2": 16, "TXD2": 17, }, + "m5stack-core2": { + "SDA": 32, + "SCL": 33, + "SS": 5, + "MOSI": 23, + "MISO": 38, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, + "m5stack-coreink": { + "SDA": 32, + "SCL": 33, + "SS": 9, + "MOSI": 23, + "MISO": 34, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, "m5stack-fire": { "ADC1": 35, "ADC2": 36, @@ -630,6 +827,17 @@ ESP32_BOARD_PINS = { "RXD2": 16, "TXD2": 17, }, + "m5stack-timer-cam": { + "LED_BUILTIN": 2, + "SDA": 4, + "SCL": 13, + "SS": 5, + "MOSI": 23, + "MISO": 19, + "SCK": 18, + "ADC1": 35, + "ADC2": 36, + }, "m5stick-c": { "ADC1": 35, "ADC2": 36, @@ -664,6 +872,17 @@ ESP32_BOARD_PINS = { "RIGHT_PUTTON": 34, "YELLOW_LED": 18, }, + "mgbot-iotik32a": { + "LED_BUILTIN": 4, + "TX2": 17, + "RX2": 16, + }, + "mgbot-iotik32b": { + "LED_BUILTIN": 18, + "IR": 27, + "TX2": 17, + "RX2": 16, + }, "mhetesp32devkit": {"LED": 2}, "mhetesp32minikit": {"LED": 2}, "microduino-core-esp32": { @@ -740,6 +959,7 @@ ESP32_BOARD_PINS = { }, "node32s": {}, "nodemcu-32s": {"BUTTON": 0, "LED": 2}, + "nscreen-32": {}, "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": { @@ -766,6 +986,10 @@ ESP32_BOARD_PINS = { "VBAT": 35, }, "pico32": {}, + "piranha_esp32": { + "LED_BUILTIN": 2, + "KEY_BUILTIN": 0, + }, "pocket_32": {"LED": 16}, "pycom_gpy": { "A1": 37, @@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = { "SDA": 12, "SS": 17, }, + "qchip": "heltec_wifi_kit_32", "quantum": {}, + "s_odi_ultra": { + "LED_BUILTIN": 2, + "LED_BUILTINB": 4, + }, + "sensesiot_weizen": {}, + "sg-o_airMon": {}, "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, "tinypico": {}, "ttgo-lora32-v1": { @@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = { "SCK": 5, "SS": 18, }, + "ttgo-lora32-v2": { + "LED_BUILTIN": 22, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "A1": 37, + "A2": 38, + }, + "ttgo-lora32-v21": { + "LED_BUILTIN": 25, + "KEY_BUILTIN": 0, + "SS": 18, + "MOSI": 27, + "MISO": 19, + "SCK": 5, + "A1": 37, + "A2": 38, + }, "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}, @@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = { "T5": 5, "T6": 4, }, + "wifiduino32": { + "LED_BUILTIN": 2, + "KEY_BUILTIN": 0, + "SDA": 5, + "SCL": 16, + "A0": 27, + "A1": 14, + "A2": 12, + "A3": 35, + "A4": 13, + "A5": 4, + "D0": 3, + "D1": 1, + "D2": 17, + "D3": 15, + "D4": 32, + "D5": 33, + "D6": 25, + "D7": 26, + "D8": 23, + "D9": 22, + "D10": 21, + "D11": 19, + "D12": 18, + "D13": 2, + }, "xinabox_cw02": {"LED": 27}, } diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 5f60fbe0b2..d9c2892d21 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { it[led].set(Color::BLACK); } + it.schedule_show(); } void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { @@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); } + it.schedule_show(); return CONSUMED; } diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py new file mode 100644 index 0000000000..ca94069703 --- /dev/null +++ b/esphome/components/airthings_ble/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ["esp32_ble_tracker"] +CODEOWNERS = ["@jeromelaban"] + +airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") +AirthingsListener = airthings_ble_ns.class_( + "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(AirthingsListener), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp new file mode 100644 index 0000000000..921e42c498 --- /dev/null +++ b/esphome/components/airthings_ble/airthings_listener.cpp @@ -0,0 +1,33 @@ +#include "airthings_listener.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace airthings_ble { + +static const char *TAG = "airthings_ble"; + +bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + for (auto &it : device.get_manufacturer_datas()) { + if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) { + if (it.data.size() < 4) + continue; + + uint32_t sn = it.data[0]; + sn |= ((uint32_t) it.data[1] << 8); + sn |= ((uint32_t) it.data[2] << 16); + sn |= ((uint32_t) it.data[3] << 24); + + ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str()); + return true; + } + } + + return false; +} + +} // namespace airthings_ble +} // namespace esphome + +#endif diff --git a/esphome/components/airthings_ble/airthings_listener.h b/esphome/components/airthings_ble/airthings_listener.h new file mode 100644 index 0000000000..cd240ac1ba --- /dev/null +++ b/esphome/components/airthings_ble/airthings_listener.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef ARDUINO_ARCH_ESP32 + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + +namespace esphome { +namespace airthings_ble { + +class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace airthings_ble +} // namespace esphome + +#endif diff --git a/esphome/components/airthings_wave_plus/__init__.py b/esphome/components/airthings_wave_plus/__init__.py new file mode 100644 index 0000000000..1aff461edd --- /dev/null +++ b/esphome/components/airthings_wave_plus/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jeromelaban"] diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp new file mode 100644 index 0000000000..6b2e807e0b --- /dev/null +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -0,0 +1,142 @@ +#include "airthings_wave_plus.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace airthings_wave_plus { + +void AirthingsWavePlus::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: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "Connected successfully!"); + } + break; + } + + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "Disconnected!"); + break; + } + + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid); + if (chr == nullptr) { + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(), + sensors_data_characteristic_uuid.to_string().c_str()); + break; + } + this->handle = chr->handle; + this->node_state = espbt::ClientState::Established; + + request_read_values_(); + break; + } + + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + read_sensors_(param->read.value, param->read.value_len); + } + break; + } + + default: + break; + } +} + +void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { + auto value = (WavePlusReadings *) raw_value; + + if (sizeof(WavePlusReadings) <= value_len) { + ESP_LOGD(TAG, "version = %d", value->version); + + if (value->version == 1) { + ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); + + this->humidity_sensor_->publish_state(value->humidity / 2.0f); + if (is_valid_radon_value_(value->radon)) { + this->radon_sensor_->publish_state(value->radon); + } + if (is_valid_radon_value_(value->radon_lt)) { + this->radon_long_term_sensor_->publish_state(value->radon_lt); + } + this->temperature_sensor_->publish_state(value->temperature / 100.0f); + this->pressure_sensor_->publish_state(value->pressure / 50.0f); + if (is_valid_co2_value_(value->co2)) { + this->co2_sensor_->publish_state(value->co2); + } + if (is_valid_voc_value_(value->voc)) { + this->tvoc_sensor_->publish_state(value->voc); + } + + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + parent()->set_enabled(false); + } else { + ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); + } + } +} + +bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; } + +bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; } + +bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; } + +void AirthingsWavePlus::loop() {} + +void AirthingsWavePlus::update() { + if (this->node_state != espbt::ClientState::Established) { + if (!parent()->enabled) { + ESP_LOGW(TAG, "Reconnecting to device"); + parent()->set_enabled(true); + parent()->connect(); + } else { + ESP_LOGW(TAG, "Connection in progress"); + } + } +} + +void AirthingsWavePlus::request_read_values_() { + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + } +} + +void AirthingsWavePlus::dump_config() { + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Radon", this->radon_sensor_); + LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); +} + +AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) { + auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative(); + auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative(); + + service_uuid = espbt::ESPBTUUID::from_uuid(service_bt); + sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt); +} + +void AirthingsWavePlus::setup() {} + +} // namespace airthings_wave_plus +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h new file mode 100644 index 0000000000..18d7fe60d2 --- /dev/null +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -0,0 +1,79 @@ +#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/core/log.h" +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 +#include +#include + +using namespace esphome::ble_client; + +namespace esphome { +namespace airthings_wave_plus { + +static const char *TAG = "airthings_wave_plus"; + +class AirthingsWavePlus : public PollingComponent, public BLEClientNode { + public: + AirthingsWavePlus(); + + void setup() override; + void dump_config() override; + void update() 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 set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } + void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } + void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } + void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + + protected: + bool is_valid_radon_value_(short radon); + bool is_valid_voc_value_(short voc); + bool is_valid_co2_value_(short co2); + + void read_sensors_(uint8_t *value, uint16_t value_len); + void request_read_values_(); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *radon_sensor_{nullptr}; + sensor::Sensor *radon_long_term_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + + uint16_t handle; + espbt::ESPBTUUID service_uuid; + espbt::ESPBTUUID sensors_data_characteristic_uuid; + + struct WavePlusReadings { + uint8_t version; + uint8_t humidity; + uint8_t ambientLight; + uint8_t unused01; + uint16_t radon; + uint16_t radon_lt; + uint16_t temperature; + uint16_t pressure; + uint16_t co2; + uint16_t voc; + }; +}; + +} // namespace airthings_wave_plus +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py new file mode 100644 index 0000000000..4109fca700 --- /dev/null +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -0,0 +1,116 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client + +from esphome.const import ( + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + ICON_RADIOACTIVE, + CONF_ID, + CONF_RADON, + CONF_RADON_LONG_TERM, + CONF_HUMIDITY, + CONF_TVOC, + CONF_CO2, + CONF_PRESSURE, + CONF_TEMPERATURE, + UNIT_BECQUEREL_PER_CUBIC_METER, + UNIT_PARTS_PER_MILLION, + UNIT_PARTS_PER_BILLION, + ICON_RADIATOR, +) + +DEPENDENCIES = ["ble_client"] + +airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") +AirthingsWavePlus = airthings_wave_plus_ns.class_( + "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode +) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AirthingsWavePlus), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=0, + ), + cv.Optional(CONF_RADON): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(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, + ), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("5mins")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + await ble_client.register_ble_node(var, config) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_RADON in config: + sens = await sensor.new_sensor(config[CONF_RADON]) + cg.add(var.set_radon(sens)) + if CONF_RADON_LONG_TERM in config: + sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) + cg.add(var.set_radon_long_term(sens)) + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure(sens)) + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2(sens)) + if CONF_TVOC in config: + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 559f8f649c..3705f0d7ca 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,3 +1,5 @@ +import base64 + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -6,6 +8,7 @@ from esphome.const import ( CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, + CONF_KEY, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, @@ -19,7 +22,7 @@ from esphome.const import ( from esphome.core import coroutine_with_priority DEPENDENCIES = ["network"] -AUTO_LOAD = ["async_tcp"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@OttoWinter"] api_ns = cg.esphome_ns.namespace("api") @@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = { "float[]": cg.std_vector.template(float), "string[]": cg.std_vector.template(cg.std_string), } +CONF_ENCRYPTION = "encryption" + + +def validate_encryption_key(value): + value = cv.string_strict(value) + try: + decoded = base64.b64decode(value, validate=True) + except ValueError as err: + raise cv.Invalid("Invalid key format, please check it's using base64") from err + + if len(decoded) != 32: + raise cv.Invalid("Encryption key must be base64 and 32 bytes long") + + # Return original data for roundtrip conversion + return value + CONFIG_SCHEMA = cv.Schema( { @@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema( ), } ), + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -92,6 +116,15 @@ async def to_code(config): cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) + if CONF_ENCRYPTION in config: + conf = config[CONF_ENCRYPTION] + decoded = base64.b64decode(conf[CONF_KEY]) + cg.add(var.set_noise_psk(list(decoded))) + cg.add_define("USE_API_NOISE") + cg.add_library("esphome/noise-c", "0.1.1") + else: + cg.add_define("USE_API_PLAINTEXT") + cg.add_define("USE_API") cg.add_global(api_ns.using) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e3ef2d7c9e..7648ffeaa2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -473,7 +473,8 @@ message ListEntitiesSensorResponse { bool force_update = 8; string device_class = 9; SensorStateClass state_class = 10; - SensorLastResetType last_reset_type = 11; + // Last reset type removed in 2021.9.0 + SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; } message SensorStateResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4a31f15e77..650f4f6f6e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" #include "esphome/core/version.h" +#include #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -18,74 +19,33 @@ namespace api { static const char *const TAG = "api.connection"; -APIConnection::APIConnection(AsyncClient *client, APIServer *parent) - : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { - this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); - this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); - this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, - this); - this->client_->onData([](void *s, AsyncClient *c, void *buf, - size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast(buf), len); }, - this); +APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) + : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + this->proto_write_buffer_.reserve(64); - this->send_buffer_.reserve(64); - this->recv_buffer_.reserve(32); - this->client_info_ = this->client_->remoteIP().toString().c_str(); +#if defined(USE_API_PLAINTEXT) + helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; +#elif defined(USE_API_NOISE) + helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; +#else +#error "No frame helper defined" +#endif +} +void APIConnection::start() { this->last_traffic_ = millis(); -} -APIConnection::~APIConnection() { delete this->client_; } -void APIConnection::on_error_(int8_t error) { this->remove_ = true; } -void APIConnection::on_disconnect_() { this->remove_ = true; } -void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); } -void APIConnection::on_data_(uint8_t *buf, size_t len) { - if (len == 0 || buf == nullptr) + + APIError err = helper_->init(); + if (err != APIError::OK) { + ESP_LOGW(TAG, "Helper init failed: %d errno=%d", (int) err, errno); + remove_ = true; return; - this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len); -} -void APIConnection::parse_recv_buffer_() { - if (this->recv_buffer_.empty() || this->remove_) - return; - - while (!this->recv_buffer_.empty()) { - if (this->recv_buffer_[0] != 0x00) { - ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); - this->on_fatal_error(); - return; - } - uint32_t i = 1; - const uint32_t size = this->recv_buffer_.size(); - uint32_t consumed; - auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); - if (!msg_size_varint.has_value()) - // not enough data there yet - return; - i += consumed; - uint32_t msg_size = msg_size_varint->as_uint32(); - - auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); - if (!msg_type_varint.has_value()) - // not enough data there yet - return; - i += consumed; - uint32_t msg_type = msg_type_varint->as_uint32(); - - if (size - i < msg_size) - // message body not fully received - return; - - uint8_t *msg = &this->recv_buffer_[i]; - this->read_message(msg_size, msg_type, msg); - if (this->remove_) - return; - // pop front - uint32_t total = i + msg_size; - this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total); - this->last_traffic_ = millis(); } + client_info_ = helper_->getpeername(); + helper_->set_log_info(client_info_); } -void APIConnection::disconnect_client() { - this->client_->close(); +void APIConnection::force_disconnect_client() { + this->helper_->close(); this->remove_ = true; } @@ -93,61 +53,74 @@ void APIConnection::loop() { if (this->remove_) return; - if (this->next_close_) { - this->disconnect_client(); - return; - } - if (!network_is_connected()) { // when network is disconnected force disconnect immediately // don't wait for timeout this->on_fatal_error(); return; } - if (this->client_->disconnected()) { - // failsafe for disconnect logic - this->on_disconnect_(); + if (this->next_close_) { + this->helper_->close(); + this->remove_ = true; return; } - this->parse_recv_buffer_(); + + APIError err = helper_->loop(); + if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Socket operation failed: %d", client_info_.c_str(), (int) err); + return; + } + ReadPacketBuffer buffer; + err = helper_->read_packet(&buffer); + if (err == APIError::WOULD_BLOCK) { + // pass + } else if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Reading failed: %d", client_info_.c_str(), (int) err); + return; + } else { + this->last_traffic_ = millis(); + // read a packet + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + if (this->remove_) + return; + } this->list_entities_iterator_.advance(); this->initial_state_iterator_.advance(); const uint32_t keepalive = 60000; + const uint32_t now = millis(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive - if (millis() - this->last_traffic_ > (keepalive * 5) / 2) { + if (now - this->last_traffic_ > (keepalive * 5) / 2) { + this->force_disconnect_client(); ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); - this->disconnect_client(); } - } else if (millis() - this->last_traffic_ > keepalive) { + } else if (now - this->last_traffic_ > keepalive) { this->sent_ping_ = true; this->send_ping_request(PingRequest()); } #ifdef USE_ESP32_CAMERA - if (this->image_reader_.available()) { - uint32_t space = this->client_->space(); - // reserve 15 bytes for metadata, and at least 64 bytes of data - if (space >= 15 + 64) { - uint32_t to_send = std::min(space - 15, this->image_reader_.available()); - auto buffer = this->create_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); - // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); - // bool done = 3; - bool done = this->image_reader_.available() == to_send; - buffer.encode_bool(3, done); - bool success = this->send_buffer(buffer, 44); + if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { + uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); + auto buffer = this->create_buffer(); + // fixed32 key = 1; + buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); + // bytes data = 2; + buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + // bool done = 3; + bool done = this->image_reader_.available() == to_send; + buffer.encode_bool(3, done); + bool success = this->send_buffer(buffer, 44); - if (success) { - this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); - } + if (success) { + this->image_reader_.consume_data(to_send); + } + if (success && done) { + this->image_reader_.return_image(); } } #endif @@ -289,6 +262,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { // Prefer level call.set_speed(msg.speed_level); } else if (msg.has_speed) { + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); } if (msg.has_direction) @@ -417,7 +391,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.force_update = sensor->get_force_update(); 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); @@ -709,8 +682,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str(); - this->client_info_ += ")"; + this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; + this->helper_->set_log_info(client_info_); ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); HelloResponse resp; @@ -745,9 +718,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); -#ifdef ARDUINO_BOARD - resp.model = ARDUINO_BOARD; -#endif + resp.model = ESPHOME_BOARD; #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif @@ -788,44 +759,31 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { if (this->remove_) return false; + if (!this->helper_->can_write_without_blocking()) + return false; - std::vector header; - header.push_back(0x00); - ProtoVarInt(buffer.get_buffer()->size()).encode(header); - ProtoVarInt(message_type).encode(header); - - size_t needed_space = buffer.get_buffer()->size() + header.size(); - - if (needed_space > this->client_->space()) { - delay(0); - if (needed_space > this->client_->space()) { - // SubscribeLogsResponse - if (message_type != 29) { - ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); - } - delay(0); - return false; - } + APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); + if (err == APIError::WOULD_BLOCK) + return false; + if (err != APIError::OK) { + on_fatal_error(); + ESP_LOGW(TAG, "%s: Packet write failed %d errno=%d", client_info_.c_str(), (int) err, errno); + return false; } - - this->client_->add(reinterpret_cast(header.data()), header.size(), - ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE); - this->client_->add(reinterpret_cast(buffer.get_buffer()->data()), buffer.get_buffer()->size(), - ASYNC_WRITE_FLAG_COPY); - bool ret = this->client_->send(); - return ret; + this->last_traffic_ = millis(); + return true; } void APIConnection::on_unauthenticated_access() { - ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); this->on_fatal_error(); + ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); } void APIConnection::on_no_setup_connection() { - ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); this->on_fatal_error(); + ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); } void APIConnection::on_fatal_error() { ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); - this->client_->close(); + this->helper_->close(); this->remove_ = true; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index bc9839a423..a1788bbede 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -5,16 +5,18 @@ #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" +#include "api_frame_helper.h" namespace esphome { namespace api { class APIConnection : public APIServerConnection { public: - APIConnection(AsyncClient *client, APIServer *parent); - virtual ~APIConnection(); + APIConnection(std::unique_ptr socket, APIServer *parent); + virtual ~APIConnection() = default; - void disconnect_client(); + void start(); + void force_disconnect_client(); void loop(); bool send_list_info_done() { @@ -87,8 +89,8 @@ class APIConnection : public APIServerConnection { #endif void on_disconnect_response(const DisconnectResponse &value) override { - // we initiated disconnect_client - this->next_close_ = true; + this->helper_->close(); + this->remove_ = true; } void on_ping_response(const PingResponse &value) override { // we initiated ping @@ -102,6 +104,8 @@ class APIConnection : public APIServerConnection { ConnectResponse connect(const ConnectRequest &msg) override; DisconnectResponse disconnect(const DisconnectRequest &msg) override { // remote initiated disconnect_client + // don't close yet, we still need to send the disconnect response + // close will happen on next loop this->next_close_ = true; DisconnectResponse resp; return resp; @@ -135,19 +139,16 @@ class APIConnection : public APIServerConnection { void on_unauthenticated_access() override; void on_no_setup_connection() override; ProtoWriteBuffer create_buffer() override { - this->send_buffer_.clear(); - return {&this->send_buffer_}; + // FIXME: ensure no recursive writes can happen + this->proto_write_buffer_.clear(); + return {&this->proto_write_buffer_}; } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; protected: friend APIServer; - void on_error_(int8_t error); - void on_disconnect_(); - void on_timeout_(uint32_t time); - void on_data_(uint8_t *buf, size_t len); - void parse_recv_buffer_(); + bool send_(const void *buf, size_t len, bool force); enum class ConnectionState { WAITING_FOR_HELLO, @@ -157,8 +158,10 @@ class APIConnection : public APIServerConnection { bool remove_{false}; - std::vector send_buffer_; - std::vector recv_buffer_; + // Buffer used to encode proto messages + // Re-use to prevent allocations + std::vector proto_write_buffer_; + std::unique_ptr helper_; std::string client_info_; #ifdef USE_ESP32_CAMERA @@ -170,9 +173,7 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; - bool current_nodelay_{false}; - bool next_close_{false}; - AsyncClient *client_; + bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp new file mode 100644 index 0000000000..26fbf1269f --- /dev/null +++ b/esphome/components/api/api_frame_helper.cpp @@ -0,0 +1,906 @@ +#include "api_frame_helper.h" + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "proto.h" + +namespace esphome { +namespace api { + +static const char *const TAG = "api.socket"; + +/// Is the given return value (from read/write syscalls) a wouldblock error? +bool is_would_block(ssize_t ret) { + if (ret == -1) { + return errno == EWOULDBLOCK || errno == EAGAIN; + } + return ret == 0; +} + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) + +#ifdef USE_API_NOISE +static const char *const PROLOGUE_INIT = "NoiseAPIInit"; + +/// Convert a noise error code to a readable error +std::string noise_err_to_str(int err) { + if (err == NOISE_ERROR_NO_MEMORY) + return "NO_MEMORY"; + if (err == NOISE_ERROR_UNKNOWN_ID) + return "UNKNOWN_ID"; + if (err == NOISE_ERROR_UNKNOWN_NAME) + return "UNKNOWN_NAME"; + if (err == NOISE_ERROR_MAC_FAILURE) + return "MAC_FAILURE"; + if (err == NOISE_ERROR_NOT_APPLICABLE) + return "NOT_APPLICABLE"; + if (err == NOISE_ERROR_SYSTEM) + return "SYSTEM"; + if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) + return "REMOTE_KEY_REQUIRED"; + if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) + return "LOCAL_KEY_REQUIRED"; + if (err == NOISE_ERROR_PSK_REQUIRED) + return "PSK_REQUIRED"; + if (err == NOISE_ERROR_INVALID_LENGTH) + return "INVALID_LENGTH"; + if (err == NOISE_ERROR_INVALID_PARAM) + return "INVALID_PARAM"; + if (err == NOISE_ERROR_INVALID_STATE) + return "INVALID_STATE"; + if (err == NOISE_ERROR_INVALID_NONCE) + return "INVALID_NONCE"; + if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) + return "INVALID_PRIVATE_KEY"; + if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) + return "INVALID_PUBLIC_KEY"; + if (err == NOISE_ERROR_INVALID_FORMAT) + return "INVALID_FORMAT"; + if (err == NOISE_ERROR_INVALID_SIGNATURE) + return "INVALID_SIGNATURE"; + return to_string(err); +} + +/// Initialize the frame helper, returns OK if successful. +APIError APINoiseFrameHelper::init() { + if (state_ != State::INITIALIZE || socket_ == nullptr) { + HELPER_LOG("Bad state for init %d", (int) state_); + return APIError::BAD_STATE; + } + int err = socket_->setblocking(false); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nonblocking failed with errno %d", errno); + return APIError::TCP_NONBLOCKING_FAILED; + } + int enable = 1; + err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nodelay failed with errno %d", errno); + return APIError::TCP_NODELAY_FAILED; + } + + // init prologue + prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); + + state_ = State::CLIENT_HELLO; + return APIError::OK; +} +/// Run through handshake messages (if in that phase) +APIError APINoiseFrameHelper::loop() { + APIError err = state_action_(); + if (err == APIError::WOULD_BLOCK) + return APIError::OK; + if (err != APIError::OK) + return err; + if (!tx_buf_.empty()) { + err = try_send_tx_buf_(); + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg_start: points to the start of the payload - this pointer is only valid until the next + * try_receive_raw_ call + * + * @return 0 if a full packet is in rx_buf_ + * @return -1 if error, check errno. + * + * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. + * errno ENOMEM: Not enough memory for reading packet. + * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. + */ +APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { + int err; + APIError aerr; + + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + if (rx_header_buf_len_ < 3) { + // no header information yet + size_t to_read = 3 - rx_header_buf_len_; + ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_header_buf_len_ += received; + if (received != to_read) { + // not a full read + return APIError::WOULD_BLOCK; + } + + // header reading done + } + + // read body + uint8_t indicator = rx_header_buf_[0]; + if (indicator != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", indicator); + return APIError::BAD_INDICATOR; + } + + uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; + + if (state_ != State::DATA && msg_size > 128) { + // for handshake message only permit up to 128 bytes + state_ = State::FAILED; + HELPER_LOG("Bad packet len for handshake: %d", msg_size); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + // reserve space for body + if (rx_buf_.size() != msg_size) { + rx_buf_.resize(msg_size); + } + + if (rx_buf_len_ < msg_size) { + // more data to read + size_t to_read = msg_size - rx_buf_len_; + ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_buf_len_ += received; + if (received != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + frame->msg = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_len_ = 0; + return APIError::OK; +} + +/** To be called from read/write methods. + * + * This method runs through the internal handshake methods, if in that state. + * + * If the handshake is still active when this method returns and a read/write can't take place at + * the moment, returns WOULD_BLOCK. + * If an error occured, returns that error. Only returns OK if the transport is ready for data + * traffic. + */ +APIError APINoiseFrameHelper::state_action_() { + int err; + APIError aerr; + if (state_ == State::INITIALIZE) { + HELPER_LOG("Bad state for method: %d", (int) state_); + return APIError::BAD_STATE; + } + if (state_ == State::CLIENT_HELLO) { + // waiting for client hello + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + // ignore contents, may be used in future for flags + prologue_.push_back((uint8_t)(frame.msg.size() >> 8)); + prologue_.push_back((uint8_t) frame.msg.size()); + prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); + + state_ = State::SERVER_HELLO; + } + if (state_ == State::SERVER_HELLO) { + // send server hello + uint8_t msg[1]; + msg[0] = 0x01; // chosen proto + aerr = write_frame_(msg, 1); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + } + if (state_ == State::HANDSHAKE) { + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + // waiting for handshake msg + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + return aerr; + } + if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + return aerr; + } + if (aerr != APIError::OK) + return aerr; + + if (frame.msg.empty()) { + send_explicit_handshake_reject_("Empty handshake message"); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } else if (frame.msg[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); + send_explicit_handshake_reject_("Bad handshake error byte"); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); + err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); + if (err != 0) { + // TODO: explicit rejection + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); + if (err == NOISE_ERROR_MAC_FAILURE) { + send_explicit_handshake_reject_("Handshake MAC failure"); + } else { + send_explicit_handshake_reject_("Handshake error"); + } + return APIError::HANDSHAKESTATE_READ_FAILED; + } + + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_WRITE_FAILED; + } + buffer[0] = 0x00; // success + + aerr = write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else { + // bad state for action + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + } + if (state_ == State::CLOSED || state_ == State::FAILED) { + return APIError::BAD_STATE; + } + return APIError::OK; +} +void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { + std::vector data; + data.reserve(reason.size() + 1); + data[0] = 0x01; // failure + for (size_t i = 0; i < reason.size(); i++) { + data[i + 1] = (uint8_t) reason[i]; + } + write_frame_(data.data(), data.size()); +} + +APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); + err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); + return APIError::CIPHERSTATE_DECRYPT_FAILED; + } + + size_t msg_size = mbuf.size; + uint8_t *msg_data = frame.msg.data(); + if (msg_size < 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: size %d too short", msg_size); + return APIError::BAD_DATA_PACKET; + } + + // uint16_t type; + // uint16_t data_len; + // uint8_t *data; + // uint8_t *padding; zero or more bytes to fill up the rest of the packet + uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; + uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; + if (data_len > msg_size - 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); + return APIError::BAD_DATA_PACKET; + } + + buffer->container = std::move(frame.msg); + buffer->data_offset = 4; + buffer->data_len = data_len; + buffer->type = type; + return APIError::OK; +} +bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } +APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + size_t padding = 0; + size_t msg_len = 4 + payload_len + padding; + size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); + auto tmpbuf = std::unique_ptr{new (std::nothrow) uint8_t[frame_len]}; + if (tmpbuf == nullptr) { + HELPER_LOG("Could not allocate for writing packet"); + return APIError::OUT_OF_MEMORY; + } + + tmpbuf[0] = 0x01; // indicator + // tmpbuf[1], tmpbuf[2] to be set later + const uint8_t msg_offset = 3; + const uint8_t payload_offset = msg_offset + 4; + tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8); // type + tmpbuf[msg_offset + 1] = (uint8_t) type; + tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8); // data_len + tmpbuf[msg_offset + 3] = (uint8_t) payload_len; + // copy data + std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); + // fill padding with zeros + std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); + err = noise_cipherstate_encrypt(send_cipher_, &mbuf); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); + return APIError::CIPHERSTATE_ENCRYPT_FAILED; + } + + size_t total_len = 3 + mbuf.size; + tmpbuf[1] = (uint8_t)(mbuf.size >> 8); + tmpbuf[2] = (uint8_t) mbuf.size; + // write raw to not have two packets sent if NAGLE disabled + aerr = write_raw_(&tmpbuf[0], total_len); + if (aerr != APIError::OK) { + return aerr; + } + return APIError::OK; +} +APIError APINoiseFrameHelper::try_send_tx_buf_() { + // try send from tx_buf + while (state_ != State::CLOSED && !tx_buf_.empty()) { + ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); + if (sent == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent == 0) { + break; + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); + } + + return APIError::OK; +} +/** Write the data to the socket, or buffer it a write would block + * + * @param data The data to write + * @param len The length of data + */ +APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) { + if (len == 0) + return APIError::OK; + int err; + APIError aerr; + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); + + if (!tx_buf_.empty()) { + // try to empty tx_buf_ first + aerr = try_send_tx_buf_(); + if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) + return aerr; + } + + if (!tx_buf_.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } + + ssize_t sent = socket_->write(data, len); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } else if (sent == -1) { + // an error occured + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent != len) { + // partially sent, add end to tx_buf + tx_buf_.insert(tx_buf_.end(), data + sent, data + len); + return APIError::OK; + } + // fully sent + return APIError::OK; +} +APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { + APIError aerr; + + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t)(len >> 8); + header[2] = (uint8_t) len; + + aerr = write_raw_(header, 3); + if (aerr != APIError::OK) + return aerr; + aerr = write_raw_(data, len); + return aerr; +} + +/** Initiate the data structures for the handshake. + * + * @return 0 on success, -1 on error (check errno) + */ +APIError APINoiseFrameHelper::init_handshake_() { + int err; + memset(&nid_, 0, sizeof(nid_)); + // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); + nid_.pattern_id = NOISE_PATTERN_NN; + nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid_.dh_id = NOISE_DH_CURVE25519; + nid_.prefix_id = NOISE_PREFIX_STANDARD; + nid_.hybrid_id = NOISE_DH_NONE; + nid_.hash_id = NOISE_HASH_SHA256; + nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + + const auto &psk = ctx_->get_psk(); + err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + + err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + // set_prologue copies it into handshakestate, so we can get rid of it now + prologue_ = {}; + + err = noise_handshakestate_start(handshake_); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SETUP_FAILED; + } + return APIError::OK; +} + +APIError APINoiseFrameHelper::check_handshake_finished_() { + assert(state_ == State::HANDSHAKE); + + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) + return APIError::OK; + if (action != NOISE_ACTION_SPLIT) { + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); + return APIError::HANDSHAKESTATE_SPLIT_FAILED; + } + + HELPER_LOG("Handshake complete!"); + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + state_ = State::DATA; + return APIError::OK; +} + +APINoiseFrameHelper::~APINoiseFrameHelper() { + if (handshake_ != nullptr) { + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + } + if (send_cipher_ != nullptr) { + noise_cipherstate_free(send_cipher_); + send_cipher_ = nullptr; + } + if (recv_cipher_ != nullptr) { + noise_cipherstate_free(recv_cipher_); + recv_cipher_ = nullptr; + } +} + +APIError APINoiseFrameHelper::close() { + state_ = State::CLOSED; + int err = socket_->close(); + if (err == -1) + return APIError::CLOSE_FAILED; + return APIError::OK; +} +APIError APINoiseFrameHelper::shutdown(int how) { + int err = socket_->shutdown(how); + if (err == -1) + return APIError::SHUTDOWN_FAILED; + if (how == SHUT_RDWR) { + state_ = State::CLOSED; + } + return APIError::OK; +} +extern "C" { +// declare how noise generates random bytes (here with a good HWRNG based on the RF system) +void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast(output), len); } +} +#endif // USE_API_NOISE + +#ifdef USE_API_PLAINTEXT + +/// Initialize the frame helper, returns OK if successful. +APIError APIPlaintextFrameHelper::init() { + if (state_ != State::INITIALIZE || socket_ == nullptr) { + HELPER_LOG("Bad state for init %d", (int) state_); + return APIError::BAD_STATE; + } + int err = socket_->setblocking(false); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nonblocking failed with errno %d", errno); + return APIError::TCP_NONBLOCKING_FAILED; + } + int enable = 1; + err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("Setting nodelay failed with errno %d", errno); + return APIError::TCP_NODELAY_FAILED; + } + + state_ = State::DATA; + return APIError::OK; +} +/// Not used for plaintext +APIError APIPlaintextFrameHelper::loop() { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + // try send pending TX data + if (!tx_buf_.empty()) { + APIError err = try_send_tx_buf_(); + if (err != APIError::OK) { + return err; + } + } + return APIError::OK; +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg: store the parsed frame in that struct + * + * @return See APIError + * + * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + */ +APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { + int err; + APIError aerr; + + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + while (!rx_header_parsed_) { + uint8_t data; + ssize_t received = socket_->read(&data, 1); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_header_buf_.push_back(data); + + // try parse header + if (rx_header_buf_[0] != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + + size_t i = 1; + size_t consumed = 0; + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + if (!msg_size_varint.has_value()) { + // not enough data there yet + continue; + } + + i += consumed; + rx_header_parsed_len_ = msg_size_varint->as_uint32(); + + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); + if (!msg_type_varint.has_value()) { + // not enough data there yet + continue; + } + rx_header_parsed_type_ = msg_type_varint->as_uint32(); + rx_header_parsed_ = true; + } + // header reading done + + // reserve space for body + if (rx_buf_.size() != rx_header_parsed_len_) { + rx_buf_.resize(rx_header_parsed_len_); + } + + if (rx_buf_len_ < rx_header_parsed_len_) { + // more data to read + size_t to_read = rx_header_parsed_len_ - rx_buf_len_; + ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); + if (is_would_block(received)) { + return APIError::WOULD_BLOCK; + } else if (received == -1) { + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } + rx_buf_len_ += received; + if (received != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); + frame->msg = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_.clear(); + rx_header_parsed_ = false; + return APIError::OK; +} + +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + ParsedFrame frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + buffer->container = std::move(frame.msg); + buffer->data_offset = 0; + buffer->data_len = rx_header_parsed_len_; + buffer->type = rx_header_parsed_type_; + return APIError::OK; +} +bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } +APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { + int err; + APIError aerr; + + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + + std::vector header; + header.push_back(0x00); + ProtoVarInt(payload_len).encode(header); + ProtoVarInt(type).encode(header); + + aerr = write_raw_(&header[0], header.size()); + if (aerr != APIError::OK) { + return aerr; + } + aerr = write_raw_(payload, payload_len); + if (aerr != APIError::OK) { + return aerr; + } + return APIError::OK; +} +APIError APIPlaintextFrameHelper::try_send_tx_buf_() { + // try send from tx_buf + while (state_ != State::CLOSED && !tx_buf_.empty()) { + ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); + if (sent == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent == 0) { + break; + } + // TODO: inefficient if multiple packets in txbuf + // replace with deque of buffers + tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); + } + + return APIError::OK; +} +/** Write the data to the socket, or buffer it a write would block + * + * @param data The data to write + * @param len The length of data + */ +APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { + if (len == 0) + return APIError::OK; + int err; + APIError aerr; + + // uncomment for even more debugging + // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); + + if (!tx_buf_.empty()) { + // try to empty tx_buf_ first + aerr = try_send_tx_buf_(); + if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) + return aerr; + } + + if (!tx_buf_.empty()) { + // tx buf not empty, can't write now because then stream would be inconsistent + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } + + ssize_t sent = socket_->write(data, len); + if (is_would_block(sent)) { + // operation would block, add buffer to tx_buf + tx_buf_.insert(tx_buf_.end(), data, data + len); + return APIError::OK; + } else if (sent == -1) { + // an error occured + state_ = State::FAILED; + HELPER_LOG("Socket write failed with errno %d", errno); + return APIError::SOCKET_WRITE_FAILED; + } else if (sent != len) { + // partially sent, add end to tx_buf + tx_buf_.insert(tx_buf_.end(), data + sent, data + len); + return APIError::OK; + } + // fully sent + return APIError::OK; +} +APIError APIPlaintextFrameHelper::write_frame_(const uint8_t *data, size_t len) { + APIError aerr; + + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t)(len >> 8); + header[2] = (uint8_t) len; + + aerr = write_raw_(header, 3); + if (aerr != APIError::OK) + return aerr; + aerr = write_raw_(data, len); + return aerr; +} + +APIError APIPlaintextFrameHelper::close() { + state_ = State::CLOSED; + int err = socket_->close(); + if (err == -1) + return APIError::CLOSE_FAILED; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::shutdown(int how) { + int err = socket_->shutdown(how); + if (err == -1) + return APIError::SHUTDOWN_FAILED; + if (how == SHUT_RDWR) { + state_ = State::CLOSED; + } + return APIError::OK; +} +#endif // USE_API_PLAINTEXT + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h new file mode 100644 index 0000000000..7189bc4b4b --- /dev/null +++ b/esphome/components/api/api_frame_helper.h @@ -0,0 +1,179 @@ +#pragma once +#include +#include +#include + +#include "esphome/core/defines.h" + +#ifdef USE_API_NOISE +#include "noise/protocol.h" +#endif + +#include "esphome/components/socket/socket.h" +#include "api_noise_context.h" + +namespace esphome { +namespace api { + +struct ReadPacketBuffer { + std::vector container; + uint16_t type; + size_t data_offset; + size_t data_len; +}; + +struct PacketBuffer { + const std::vector container; + uint16_t type; + uint8_t data_offset; + uint8_t data_len; +}; + +enum class APIError : int { + OK = 0, + WOULD_BLOCK = 1001, + BAD_HANDSHAKE_PACKET_LEN = 1002, + BAD_INDICATOR = 1003, + BAD_DATA_PACKET = 1004, + TCP_NODELAY_FAILED = 1005, + TCP_NONBLOCKING_FAILED = 1006, + CLOSE_FAILED = 1007, + SHUTDOWN_FAILED = 1008, + BAD_STATE = 1009, + BAD_ARG = 1010, + SOCKET_READ_FAILED = 1011, + SOCKET_WRITE_FAILED = 1012, + HANDSHAKESTATE_READ_FAILED = 1013, + HANDSHAKESTATE_WRITE_FAILED = 1014, + HANDSHAKESTATE_BAD_STATE = 1015, + CIPHERSTATE_DECRYPT_FAILED = 1016, + CIPHERSTATE_ENCRYPT_FAILED = 1017, + OUT_OF_MEMORY = 1018, + HANDSHAKESTATE_SETUP_FAILED = 1019, + HANDSHAKESTATE_SPLIT_FAILED = 1020, +}; + +class APIFrameHelper { + public: + virtual APIError init() = 0; + virtual APIError loop() = 0; + virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; + virtual bool can_write_without_blocking() = 0; + virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; + virtual std::string getpeername() = 0; + virtual APIError close() = 0; + virtual APIError shutdown(int how) = 0; + // Give this helper a name for logging + virtual void set_log_info(std::string info) = 0; +}; + +#ifdef USE_API_NOISE +class APINoiseFrameHelper : public APIFrameHelper { + public: + APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) + : socket_(std::move(socket)), ctx_(ctx) {} + ~APINoiseFrameHelper(); + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + bool can_write_without_blocking() override; + APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + std::string getpeername() override { return socket_->getpeername(); } + APIError close() override; + APIError shutdown(int how) override; + // Give this helper a name for logging + void set_log_info(std::string info) override { info_ = std::move(info); } + + protected: + struct ParsedFrame { + std::vector msg; + }; + + APIError state_action_(); + APIError try_read_frame_(ParsedFrame *frame); + APIError try_send_tx_buf_(); + APIError write_frame_(const uint8_t *data, size_t len); + APIError write_raw_(const uint8_t *data, size_t len); + APIError init_handshake_(); + APIError check_handshake_finished_(); + void send_explicit_handshake_reject_(const std::string &reason); + + std::unique_ptr socket_; + + std::string info_; + uint8_t rx_header_buf_[3]; + size_t rx_header_buf_len_ = 0; + std::vector rx_buf_; + size_t rx_buf_len_ = 0; + + std::vector tx_buf_; + std::vector prologue_; + + std::shared_ptr ctx_; + NoiseHandshakeState *handshake_ = nullptr; + NoiseCipherState *send_cipher_ = nullptr; + NoiseCipherState *recv_cipher_ = nullptr; + NoiseProtocolId nid_; + + enum class State { + INITIALIZE = 1, + CLIENT_HELLO = 2, + SERVER_HELLO = 3, + HANDSHAKE = 4, + DATA = 5, + CLOSED = 6, + FAILED = 7, + } state_ = State::INITIALIZE; +}; +#endif // USE_API_NOISE + +#ifdef USE_API_PLAINTEXT +class APIPlaintextFrameHelper : public APIFrameHelper { + public: + APIPlaintextFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + ~APIPlaintextFrameHelper() = default; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + bool can_write_without_blocking() override; + APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; + std::string getpeername() override { return socket_->getpeername(); } + APIError close() override; + APIError shutdown(int how) override; + // Give this helper a name for logging + void set_log_info(std::string info) override { info_ = std::move(info); } + + protected: + struct ParsedFrame { + std::vector msg; + }; + + APIError try_read_frame_(ParsedFrame *frame); + APIError try_send_tx_buf_(); + APIError write_frame_(const uint8_t *data, size_t len); + APIError write_raw_(const uint8_t *data, size_t len); + + std::unique_ptr socket_; + + std::string info_; + std::vector rx_header_buf_; + bool rx_header_parsed_ = false; + uint32_t rx_header_parsed_type_ = 0; + uint32_t rx_header_parsed_len_ = 0; + + std::vector rx_buf_; + size_t rx_buf_len_ = 0; + + std::vector tx_buf_; + + enum class State { + INITIALIZE = 1, + DATA = 2, + CLOSED = 3, + FAILED = 4, + } state_ = State::INITIALIZE; +}; +#endif + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h new file mode 100644 index 0000000000..fba6b65a26 --- /dev/null +++ b/esphome/components/api/api_noise_context.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include "esphome/core/defines.h" + +namespace esphome { +namespace api { + +#ifdef USE_API_NOISE +using psk_t = std::array; + +class APINoiseContext { + public: + void set_psk(psk_t psk) { psk_ = std::move(psk); } + const psk_t &get_psk() const { return psk_; } + + protected: + psk_t psk_; +}; +#endif // USE_API_NOISE + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f5860bee64..d6b85d257c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va return true; } case 11: { - this->last_reset_type = value.as_enum(); + this->legacy_last_reset_type = value.as_enum(); return true; } case 12: { @@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->force_update); buffer.encode_string(9, this->device_class); buffer.encode_enum(10, this->state_class); - buffer.encode_enum(11, this->last_reset_type); + buffer.encode_enum(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->state_class)); out.append("\n"); - out.append(" last_reset_type: "); - out.append(proto_enum_to_string(this->last_reset_type)); + out.append(" legacy_last_reset_type: "); + out.append(proto_enum_to_string(this->legacy_last_reset_type)); out.append("\n"); out.append(" disabled_by_default: "); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 93bfcd9b55..1371ab5248 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; - enums::SensorLastResetType last_reset_type{}; + enums::SensorLastResetType legacy_last_reset_type{}; bool disabled_by_default{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d48c0a4fd8..c4c193b389 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -1,10 +1,11 @@ #include "api_server.h" #include "api_connection.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" -#include "esphome/core/util.h" #include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" #include "esphome/core/version.h" +#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -21,20 +22,45 @@ static const char *const TAG = "api"; void APIServer::setup() { ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); this->setup_controller(); - this->server_ = AsyncServer(this->port_); - this->server_.setNoDelay(false); - this->server_.begin(); - this->server_.onClient( - [](void *s, AsyncClient *client) { - if (client == nullptr) - return; + socket_ = socket::socket(AF_INET, SOCK_STREAM, 0); + if (socket_ == nullptr) { + ESP_LOGW(TAG, "Could not create socket."); + this->mark_failed(); + return; + } + int enable = 1; + int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = socket_->setblocking(false); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + struct sockaddr_in server; + memset(&server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_addr.s_addr = ESPHOME_INADDR_ANY; + server.sin_port = htons(this->port_); + + err = socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + return; + } + + err = socket_->listen(4); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); + this->mark_failed(); + return; + } - // can't print here because in lwIP thread - // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str()); - auto *a_this = (APIServer *) s; - a_this->clients_.push_back(new APIConnection(client, a_this)); - }, - this); #ifdef USE_LOGGER if (logger::global_logger != nullptr) { logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { @@ -59,6 +85,20 @@ void APIServer::setup() { #endif } void APIServer::loop() { + // Accept new clients + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); + + auto *conn = new APIConnection(std::move(sock), this); + clients_.push_back(conn); + conn->start(); + } + // Partition clients into remove and active auto new_end = std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; }); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 96b3192e9e..e3fa6b18c9 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -4,19 +4,14 @@ #include "esphome/core/controller.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" +#include "esphome/components/socket/socket.h" #include "api_pb2.h" #include "api_pb2_service.h" #include "util.h" #include "list_entities.h" #include "subscribe_state.h" #include "user_services.h" - -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include -#endif +#include "api_noise_context.h" namespace esphome { namespace api { @@ -35,6 +30,12 @@ class APIServer : public Component, public Controller { void set_port(uint16_t port); void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); + +#ifdef USE_API_NOISE + void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); } + std::shared_ptr get_noise_ctx() { return noise_ctx_; } +#endif // USE_API_NOISE + void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; @@ -86,7 +87,7 @@ class APIServer : public Component, public Controller { const std::vector &get_user_services() const { return this->user_services_; } protected: - AsyncServer server_{0}; + std::unique_ptr socket_ = nullptr; uint16_t port_{6053}; uint32_t reboot_timeout_{300000}; uint32_t last_connected_{0}; @@ -94,6 +95,10 @@ class APIServer : public Component, public Controller { std::string password_; std::vector state_subs_; std::vector user_services_; + +#ifdef USE_API_NOISE + std::shared_ptr noise_ctx_ = std::make_shared(); +#endif // USE_API_NOISE }; extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 28b49604ff..05e5250d89 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -19,8 +19,8 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_LIGHTBULB, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, @@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index eaf41829bb..2201fe576e 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -55,7 +55,10 @@ void BinaryFan::loop() { ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); } } -float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } + +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } } // namespace binary } // namespace esphome diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 3c2169a922..9cd2a045ff 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -48,6 +48,7 @@ from esphome.const import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) @@ -79,6 +80,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index c58d29e6be..2a242c3aca 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,7 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, + CONF_IBEACON_MAJOR, + CONF_IBEACON_MINOR, + CONF_IBEACON_UUID, + CONF_ID, +) DEPENDENCIES = ["esp32_ble_tracker"] @@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_( esp32_ble_tracker.ESPBTDeviceListener, ) + +def _validate(config): + if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config: + raise cv.Invalid("iBeacon major identifier requires iBeacon UUID") + if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config: + raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID") + return config + + CONFIG_SCHEMA = cv.All( binary_sensor.BINARY_SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(BLEPresenceDevice), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, + cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, + cv.Optional(CONF_IBEACON_UUID): cv.uuid, } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA), - cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID), + cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), + _validate, ) @@ -50,5 +70,15 @@ async def to_code(config): ) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) + + if CONF_IBEACON_UUID in config: + ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID])) + cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) + + if CONF_IBEACON_MAJOR in config: + cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR])) + + if CONF_IBEACON_MINOR in config: + cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index bce6a9cf98..dfc36d68cb 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public Component { public: void set_address(uint64_t address) { - this->by_address_ = true; + this->match_by_ = MATCH_BY_MAC_ADDRESS; this->address_ = address; } void set_service_uuid16(uint16_t uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { - this->by_address_ = false; + this->match_by_ = MATCH_BY_SERVICE_UUID; this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); } + void set_ibeacon_uuid(uint8_t *uuid) { + this->match_by_ = MATCH_BY_IBEACON_UUID; + this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } + void set_ibeacon_major(uint16_t major) { + this->check_ibeacon_major_ = true; + this->ibeacon_major_ = major; + } + void set_ibeacon_minor(uint16_t minor) { + this->check_ibeacon_minor_ = true; + this->ibeacon_minor_ = minor; + } void on_scan_end() override { if (!this->found_) this->publish_state(false); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (this->by_address_) { - if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; - return true; - } - } else { - for (auto uuid : device.get_service_uuids()) { - if (this->uuid_ == uuid) { - this->publish_state(device.get_rssi()); + switch (this->match_by_) { + case MATCH_BY_MAC_ADDRESS: + if (device.address_uint64() == this->address_) { + this->publish_state(true); this->found_ = true; return true; } - } + break; + case MATCH_BY_SERVICE_UUID: + for (auto uuid : device.get_service_uuids()) { + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } + break; + case MATCH_BY_IBEACON_UUID: + if (!device.get_ibeacon().has_value()) { + return false; + } + + auto ibeacon = device.get_ibeacon().value(); + + if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { + return false; + } + + if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) { + return false; + } + + if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) { + return false; + } + + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; } return false; } @@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, float get_setup_priority() const override { return setup_priority::DATA; } protected: + enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; + MATCH_TYPE match_by_; + bool found_{false}; - bool by_address_{false}; + uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; + + esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; + uint16_t ibeacon_major_; + bool check_ibeacon_major_; + uint16_t ibeacon_minor_; + bool check_ibeacon_minor_; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index bca73328f9..0c4308b11a 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -60,5 +60,5 @@ async def to_code(config): ) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 8f53180296..e2cb7491a6 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; -BME680BSECComponent *BME680BSECComponent::instance; +BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void BME680BSECComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); @@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va sensor->publish_state(value); } -void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 73994b7541..365aec725e 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { int64_t get_time_ns_(); void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); - void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); + void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); void load_state_(); void save_state_(uint8_t accuracy); diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index dec070a9b2..08df6f7774 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -16,7 +16,7 @@ static const char *const TAG = "ccs811"; return; \ } -#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED) +#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED) void CCS811Component::setup() { // page 9 programming guide - hwid is always 0x81 @@ -38,12 +38,14 @@ void CCS811Component::setup() { // set MEAS_MODE (page 5) uint8_t meas_mode = 0; uint32_t interval = this->get_update_interval(); - if (interval <= 1000) - meas_mode = 1 << 4; - else if (interval <= 10000) - meas_mode = 2 << 4; + if (interval >= 60 * 1000) + meas_mode = 3 << 4; // sensor takes a reading every 60 seconds + else if (interval >= 10 * 1000) + meas_mode = 2 << 4; // sensor takes a reading every 10 seconds + else if (interval >= 1 * 1000) + meas_mode = 1 << 4; // sensor takes a reading every second else - meas_mode = 3 << 4; + meas_mode = 4 << 4; // sensor takes a reading every 250ms CHECKED_IO(this->write_byte(0x01, meas_mode)) @@ -51,6 +53,36 @@ void CCS811Component::setup() { // baseline available, write to sensor this->write_bytes(0x11, decode_uint16(*this->baseline_)); } + + auto hardware_version_data = this->read_bytes<1>(0x21); + auto bootloader_version_data = this->read_bytes<2>(0x23); + auto application_version_data = this->read_bytes<2>(0x24); + + uint8_t hardware_version = 0; + uint16_t bootloader_version = 0; + uint16_t application_version = 0; + + if (hardware_version_data.has_value()) { + hardware_version = (*hardware_version_data)[0]; + } + + if (bootloader_version_data.has_value()) { + bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]); + } + + if (application_version_data.has_value()) { + application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]); + } + + ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version, + bootloader_version, application_version); + if (this->version_ != nullptr) { + char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room + sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15), + (application_version >> 4 & 15), application_version); + ESP_LOGD(TAG, "publishing version state: %s", version); + this->version_->publish_state(version); + } } void CCS811Component::update() { if (!this->status_has_data_()) @@ -117,6 +149,7 @@ void CCS811Component::dump_config() { LOG_UPDATE_INTERVAL(this) LOG_SENSOR(" ", "CO2 Sensor", this->co2_) LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) + LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) if (this->baseline_) { ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); } else { @@ -124,7 +157,7 @@ void CCS811Component::dump_config() { } if (this->is_failed()) { switch (this->error_code_) { - case COMMUNICAITON_FAILED: + case COMMUNICATION_FAILED: ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); break; case INVALID_ID: diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index cea919c9a5..8a0d60d002 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { public: void set_co2(sensor::Sensor *co2) { co2_ = co2; } void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_version(text_sensor::TextSensor *version) { version_ = version; } void set_baseline(uint16_t baseline) { baseline_ = baseline; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } @@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { enum ErrorCode { UNKNOWN, - COMMUNICAITON_FAILED, + COMMUNICATION_FAILED, INVALID_ID, SENSOR_REPORTED_ERROR, APP_INVALID, @@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; + text_sensor::TextSensor *version_{nullptr}; optional baseline_{}; /// Input sensor for humidity reading. sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 4c09a14c3e..bb8200273d 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -1,9 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, text_sensor from esphome.const import ( + CONF_ICON, CONF_ID, ICON_RADIATOR, + ICON_RESTART, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -12,9 +16,12 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_TVOC, CONF_HUMIDITY, + CONF_VERSION, ICON_MOLECULE_CO2, ) +AUTO_LOAD = ["text_sensor"] +CODEOWNERS = ["@habbie"] DEPENDENCIES = ["i2c"] ccs811_ns = cg.esphome_ns.namespace("ccs811") @@ -30,14 +37,22 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon, + } + ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor), @@ -58,6 +73,11 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_tvoc(sens)) + if CONF_VERSION in config: + sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID]) + await text_sensor.register_text_sensor(sens, config[CONF_VERSION]) + cg.add(var.set_version(sens)) + if CONF_BASELINE in config: cg.add(var.set_baseline(config[CONF_BASELINE])) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index f6a9fa2927..c2f07ce423 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) ClimatePreset = climate_ns.enum("ClimatePreset") CLIMATE_PRESETS = { + "NONE": ClimatePreset.CLIMATE_PRESET_NONE, "ECO": ClimatePreset.CLIMATE_PRESET_ECO, "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 8da2206f37..4861e7b8cb 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } +template bool set_alternative(optional &dst, optional &alt, const T1 &src) { + bool is_changed = alt.has_value(); + alt.reset(); + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + +bool Climate::set_fan_mode_(ClimateFanMode mode) { + return set_alternative(this->fan_mode, this->custom_fan_mode, mode); +} + +bool Climate::set_custom_fan_mode_(const std::string &mode) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode); +} + +bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } + +bool Climate::set_custom_preset_(const std::string &preset) { + return set_alternative(this->custom_preset, this->preset, preset); +} + +void Climate::dump_traits_(const char *tag) { + auto traits = this->get_traits(); + ESP_LOGCONFIG(tag, "ClimateTraits:"); + ESP_LOGCONFIG(tag, " [x] Visual settings:"); + ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step()); + if (traits.get_supports_current_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + if (traits.get_supports_two_point_target_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); + if (traits.get_supports_action()) + ESP_LOGCONFIG(tag, " [x] Supports action"); + if (!traits.get_supported_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported modes:"); + for (ClimateMode m : traits.get_supported_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_mode_to_string(m)); + } + if (!traits.get_supported_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported fan modes:"); + for (ClimateFanMode m : traits.get_supported_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_fan_mode_to_string(m)); + } + if (!traits.get_supported_custom_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:"); + for (const std::string &s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported presets:"); + for (ClimatePreset p : traits.get_supported_presets()) + ESP_LOGCONFIG(tag, " - %s", climate_preset_to_string(p)); + } + if (!traits.get_supported_custom_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom presets:"); + for (const std::string &s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_swing_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported swing modes:"); + for (ClimateSwingMode m : traits.get_supported_swing_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_swing_mode_to_string(m)); + } +} + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b208e5946a..46d0fb1d77 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -245,6 +245,18 @@ class Climate : public Nameable { protected: friend ClimateCall; + /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. + bool set_fan_mode_(ClimateFanMode mode); + + /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. + bool set_custom_fan_mode_(const std::string &mode); + + /// Set preset. Reset custom preset. Return true if preset has been changed. + bool set_preset_(ClimatePreset preset); + + /// Set custom preset. Reset primary preset. Return true if preset has been changed. + bool set_custom_preset_(const std::string &preset); + /** Get the default traits of this climate device. * * Traits are static data that encode the capabilities and static data for a climate device such as supported @@ -270,6 +282,7 @@ class Climate : public Nameable { void save_state_(); uint32_t hash_base() override; + void dump_traits_(const char *tag); CallbackManager state_callback_{}; ESPPreferenceObject rtc_; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 48493b500c..903ce085d8 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -72,6 +72,7 @@ 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); } + void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } 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", "v1.20") @@ -104,6 +105,7 @@ class ClimateTraits { void set_supported_presets(std::set presets) { supported_presets_ = std::move(presets); } void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } + void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } bool get_supports_presets() const { return !supported_presets_.empty(); } const std::set &get_supported_presets() const { return supported_presets_; } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 8f30750fbd..77a53f11c5 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -125,16 +125,19 @@ class Cover : public Nameable { * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9") void open(); /** Close the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9") void close(); /** Stop the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. */ + ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9") void stop(); void add_on_state_callback(std::function &&f); diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index b3ea47d869..fae8a2b07d 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -19,7 +19,6 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INVERTED, - CONF_LAST_RESET_TYPE, CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_NAME, @@ -40,8 +39,8 @@ from esphome.const import ( ICON_BLUR, ICON_EMPTY, ICON_THERMOMETER, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, UNIT_EMPTY, UNIT_PERCENT, @@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema( 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, + CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, }, ], ): [ diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index 117468793b..344aaf26f8 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -11,8 +11,8 @@ 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) { + bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING; + if (increasing) { float base = isnan(this->state) ? 0.0f : this->state; this->publish_state(base + val * 10); } else { diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 84b263c2d5..2c05651d67 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -9,9 +9,9 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_EMPTY, - LAST_RESET_TYPE_NEVER, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_EMPTY, UNIT_VOLT, @@ -26,52 +26,22 @@ 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, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_returned_lux"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( - "kWh", - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "kWh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("total_imported_energy"): sensor.sensor_schema( "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE @@ -176,20 +146,10 @@ CONFIG_SCHEMA = cv.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_GAS, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "m³", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING ), cv.Optional("gas_delivered_be"): sensor.sensor_schema( - "m³", - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_NEVER, + "m³", ICON_EMPTY, 3, DEVICE_CLASS_GAS, STATE_CLASS_TOTAL_INCREASING ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index f280b5bc94..f0f165b25f 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet break; } + it->schedule_show(); return true; } diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index db83eb6bee..f1eebf3c8a 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() { this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); model->set_value(this->model_.value()); } else { -#ifdef ARDUINO_BOARD BLECharacteristic *model = this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(ARDUINO_BOARD); -#endif + model->set_value(ESPHOME_BOARD); } BLECharacteristic *version = diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 18f1c46ff2..fec0f6dcfb 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -108,6 +108,16 @@ def as_hex(value): def as_hex_array(value): + value = value.replace("-", "") + cpp_array = [ + f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] + ] + return cg.RawExpression( + "(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array)) + ) + + +def as_reversed_hex_array(value): value = value.replace("-", "") cpp_array = [ f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] @@ -193,7 +203,7 @@ async def to_code(config): elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): - uuid128 = as_hex_array(conf[CONF_SERVICE_UUID]) + uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID]) cg.add(trigger.set_service_uuid128(uuid128)) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) @@ -205,7 +215,7 @@ async def to_code(config): elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): - uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID]) + uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID]) cg.add(trigger.set_manufacturer_uuid128(uuid128)) if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b3db651655..e1cd3975e8 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e } for (auto &data : this->manufacturer_datas_) { ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str()); + if (this->get_ibeacon().has_value()) { + auto ibeacon = this->get_ibeacon().value(); + ESP_LOGVV(TAG, " iBeacon data:"); + ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str()); + ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major()); + ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor()); + ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power()); + } } for (auto &data : this->service_datas_) { ESP_LOGVV(TAG, " Service data:"); diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 37a9f3efbf..d725e90edc 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -1,9 +1,10 @@ #include "esp8266_pwm.h" +#include "esphome/core/macros.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#error ESP8266 PWM requires at least arduino_core_version 2.4.0 +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) +#error ESP8266 PWM requires at least arduino_version 2.4.0 #endif #include diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 8f6e2bece4..110a8d95ed 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -1,13 +1,12 @@ import re import logging from pathlib import Path -import subprocess -import hashlib -import datetime import esphome.config_validation as cv from esphome.const import ( CONF_COMPONENTS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_URL, CONF_TYPE, @@ -15,7 +14,7 @@ from esphome.const import ( CONF_PATH, ) from esphome.core import CORE -from esphome import loader +from esphome import git, loader _LOGGER = logging.getLogger(__name__) @@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS TYPE_GIT = "git" TYPE_LOCAL = "local" -CONF_REFRESH = "refresh" -CONF_REF = "ref" - - -def validate_git_ref(value): - if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: - raise cv.Invalid("Not a valid git ref") - return value GIT_SCHEMA = { cv.Required(CONF_URL): cv.url, - cv.Optional(CONF_REF): validate_git_ref, + cv.Optional(CONF_REF): cv.git_ref, } LOCAL_SCHEMA = { cv.Required(CONF_PATH): cv.directory, @@ -68,14 +59,6 @@ def validate_source_shorthand(value): return SOURCE_SCHEMA(conf) -def validate_refresh(value: str): - if value.lower() == "always": - return validate_refresh("0s") - if value.lower() == "never": - return validate_refresh("1000y") - return cv.positive_time_period_seconds(value) - - SOURCE_SCHEMA = cv.Any( validate_source_shorthand, cv.typed_schema( @@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any( CONFIG_SCHEMA = cv.ensure_list( { cv.Required(CONF_SOURCE): SOURCE_SCHEMA, - cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh), + cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( "all", cv.ensure_list(cv.string) ), @@ -102,65 +85,13 @@ async def to_code(config): pass -def _compute_destination_path(key: str) -> Path: - base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN - h = hashlib.new("sha256") - h.update(key.encode()) - return base_dir / h.hexdigest()[:8] - - -def _run_git_command(cmd, cwd=None): - try: - ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) - except FileNotFoundError as err: - raise cv.Invalid( - "git is not installed but required for external_components.\n" - "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" - ) from err - - if ret.returncode != 0 and ret.stderr: - err_str = ret.stderr.decode("utf-8") - lines = [x.strip() for x in err_str.splitlines()] - if lines[-1].startswith("fatal:"): - raise cv.Invalid(lines[-1][len("fatal: ") :]) - raise cv.Invalid(err_str) - - def _process_git_config(config: dict, refresh) -> str: - key = f"{config[CONF_URL]}@{config.get(CONF_REF)}" - repo_dir = _compute_destination_path(key) - if not repo_dir.is_dir(): - _LOGGER.info("Cloning %s", key) - _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - if CONF_REF in config: - cmd += ["--branch", config[CONF_REF]] - cmd += ["--", config[CONF_URL], str(repo_dir)] - _run_git_command(cmd) - - else: - # Check refresh needed - file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") - # On first clone, FETCH_HEAD does not exists - if not file_timestamp.exists(): - file_timestamp = Path(repo_dir / ".git" / "HEAD") - age = datetime.datetime.now() - datetime.datetime.fromtimestamp( - file_timestamp.stat().st_mtime - ) - if age.total_seconds() > refresh.total_seconds: - _LOGGER.info("Updating %s", key) - _LOGGER.debug("Location: %s", repo_dir) - # Stash local changes (if any) - _run_git_command( - ["git", "stash", "push", "--include-untracked"], str(repo_dir) - ) - # Fetch remote ref - cmd = ["git", "fetch", "--", "origin"] - if CONF_REF in config: - cmd.append(config[CONF_REF]) - _run_git_command(cmd, str(repo_dir)) - # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) - _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + repo_dir = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=refresh, + domain=DOMAIN, + ) if (repo_dir / "esphome" / "components").is_dir(): components_dir = repo_dir / "esphome" / "components" diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 9db2e9ed12..46ff0c2d53 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -15,9 +15,11 @@ from esphome.const import ( CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_NAME, + CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, + CONF_DIRECTION, ) from esphome.core import CORE, coroutine_with_priority @@ -27,6 +29,12 @@ fan_ns = cg.esphome_ns.namespace("fan") FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component) MakeFan = cg.Application.struct("MakeFan") +FanDirection = fan_ns.enum("FanDirection") +FAN_DIRECTION_ENUM = { + "FORWARD": FanDirection.FAN_DIRECTION_FORWARD, + "REVERSE": FanDirection.FAN_DIRECTION_REVERSE, +} + # Actions TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action) TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) @@ -34,6 +42,10 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) +FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) + +FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) +FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -61,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), } ), + cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), + } + ), } ) @@ -100,6 +117,9 @@ async def setup_fan_core_(var, config): for conf in config.get(CONF_ON_TURN_OFF, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_SPEED_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_fan(var, config): @@ -143,6 +163,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(FanState), cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)), + cv.Optional(CONF_DIRECTION): cv.templatable( + cv.enum(FAN_DIRECTION_ENUM, upper=True) + ), } ), ) @@ -155,9 +178,35 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): if CONF_SPEED in config: template_ = await cg.templatable(config[CONF_SPEED], args, int) cg.add(var.set_speed(template_)) + if CONF_DIRECTION in config: + template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection) + cg.add(var.set_direction(template_)) return var +@automation.register_condition( + "fan.is_on", + FanIsOnCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } + ), +) +@automation.register_condition( + "fan.is_off", + FanIsOffCondition, + automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(FanState), + } + ), +) +async def fan_is_on_off_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_FAN") diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index fbfc71c720..7ff7c720df 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -13,6 +13,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(bool, oscillating) TEMPLATABLE_VALUE(int, speed) + TEMPLATABLE_VALUE(FanDirection, direction) void play(Ts... x) override { auto call = this->state_->turn_on(); @@ -22,6 +23,9 @@ template class TurnOnAction : public Action { if (this->speed_.has_value()) { call.set_speed(this->speed_.value(x...)); } + if (this->direction_.has_value()) { + call.set_direction(this->direction_.value(x...)); + } call.perform(); } @@ -46,6 +50,23 @@ template class ToggleAction : public Action { FanState *state_; }; +template class FanIsOnCondition : public Condition { + public: + explicit FanIsOnCondition(FanState *state) : state_(state) {} + bool check(Ts... x) override { return this->state_->state; } + + protected: + FanState *state_; +}; +template class FanIsOffCondition : public Condition { + public: + explicit FanIsOffCondition(FanState *state) : state_(state) {} + bool check(Ts... x) override { return !this->state_->state; } + + protected: + FanState *state_; +}; + class FanTurnOnTrigger : public Trigger<> { public: FanTurnOnTrigger(FanState *state) { @@ -82,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> { bool last_on_; }; +class FanSpeedSetTrigger : public Trigger<> { + public: + FanSpeedSetTrigger(FanState *state) { + state->add_on_state_callback([this, state]() { + auto speed = state->speed; + auto should_trigger = speed != !this->last_speed_; + this->last_speed_ = speed; + if (should_trigger) { + this->trigger(); + } + }); + this->last_speed_ = state->speed; + } + + protected: + int last_speed_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp index 09be20991b..5d923a1b15 100644 --- a/esphome/components/fan/fan_helpers.cpp +++ b/esphome/components/fan/fan_helpers.cpp @@ -4,12 +4,14 @@ namespace esphome { namespace fan { +// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); - return static_cast(legacy_level - 1); + return static_cast(legacy_level - 1); // NOLINT(clang-diagnostic-deprecated-declarations) } +// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { const auto enum_level = static_cast(speed) + 1; const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index 9b4ae53937..a4883c5e2c 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -39,7 +39,7 @@ void FanState::setup() { call.set_direction(recovered.direction); call.perform(); } -float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } +float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; } uint32_t FanState::hash_base() { return 418001110UL; } void FanStateCall::perform() const { diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index a0dda4083a..af00275df0 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -10,7 +10,7 @@ namespace esphome { namespace fan { /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon -enum FanSpeed { +enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { FAN_SPEED_LOW = 0, ///< The fan is running on low speed. FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. @@ -45,6 +45,7 @@ class FanStateCall { this->speed_ = speed; return *this; } + ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") FanStateCall &set_speed(const char *legacy_speed); FanStateCall &set_direction(FanDirection direction) { this->direction_ = direction; diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 4d791f5709..edfeb401f1 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() { ESP_LOGCONFIG(TAG, " Num LEDs: %u", this->num_leds_); ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); } -void FastLEDLightOutput::loop() { - if (!this->should_show_()) - return; - - uint32_t now = micros(); +void FastLEDLightOutput::write_state(light::LightState *state) { // protect from refreshing too often + uint32_t now = micros(); if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + // try again next loop iteration, so that this change won't get lost + this->schedule_show(); return; } this->last_refresh_ = now; diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index ac6acc95a5..ee85735dea 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight { } void setup() override; void dump_config() override; - void loop() override; + void write_state(light::LightState *state) override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void clear_effect_data() override { diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 8d789bbcfc..9e58f672c7 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { } } - // Validate footer - if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - for (uint8_t byte = 0; byte < recv_message_length; ++byte) { ESP_LOGVV(TAG, "%02X", recv_message[byte]); } diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 1d685b9b2e..3ec12d5b83 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -13,9 +13,8 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_DEGREES, UNIT_HERTZ, @@ -143,25 +142,23 @@ CONFIG_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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_HOURS, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py index e69de29bb2..7eae863ff5 100644 --- a/esphome/components/hbridge/__init__.py +++ b/esphome/components/hbridge/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +hbridge_ns = cg.esphome_ns.namespace("hbridge") diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py new file mode 100644 index 0000000000..b169978acd --- /dev/null +++ b/esphome/components/hbridge/fan/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.components import fan, output +from esphome.const import ( + CONF_ID, + CONF_DECAY_MODE, + CONF_SPEED_COUNT, + CONF_PIN_A, + CONF_PIN_B, + CONF_ENABLE_PIN, +) +from .. import hbridge_ns + + +CODEOWNERS = ["@WeekendWarrior"] + + +HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState) + +DecayMode = hbridge_ns.enum("DecayMode") +DECAY_MODE_OPTIONS = { + "SLOW": DecayMode.DECAY_MODE_SLOW, + "FAST": DecayMode.DECAY_MODE_FAST, +} + +# Actions +BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) + + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "fan.hbridge.brake", + BrakeAction, + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), +) +async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_SPEED_COUNT], + config[CONF_DECAY_MODE], + ) + await fan.register_fan(var, config) + pin_a_ = await cg.get_variable(config[CONF_PIN_A]) + cg.add(var.set_pin_a(pin_a_)) + pin_b_ = await cg.get_variable(config[CONF_PIN_B]) + cg.add(var.set_pin_b(pin_b_)) + + if CONF_ENABLE_PIN in config: + enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) + cg.add(var.set_enable_pin(enable_pin)) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp new file mode 100644 index 0000000000..a4e5429ff4 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -0,0 +1,85 @@ +#include "hbridge_fan.h" +#include "esphome/components/fan/fan_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hbridge { + +static const char *const TAG = "fan.hbridge"; + +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level); +} + +// constant IN1/IN2, PWM on EN => power control, fast current decay +// constant IN1/EN, PWM on IN2 => power control, slow current decay +void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) { + this->pin_a_->set_level(a_level); + this->pin_b_->set_level(b_level); + this->enable_->set_level(enable); + ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable); +} + +fan::FanStateCall HBridgeFan::brake() { + ESP_LOGD(TAG, "Braking"); + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f); + return this->make_call().set_state(false); +} + +void HBridgeFan::dump_config() { + ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str()); + if (this->get_traits().supports_oscillation()) { + ESP_LOGCONFIG(TAG, " Oscillation: YES"); + } + if (this->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } + if (this->decay_mode_ == DECAY_MODE_SLOW) { + ESP_LOGCONFIG(TAG, " Decay Mode: Slow"); + } else { + ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); + } +} +void HBridgeFan::setup() { + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->set_traits(traits); + this->add_on_state_callback([this]() { this->next_update_ = true; }); +} +void HBridgeFan::loop() { + if (!this->next_update_) { + return; + } + this->next_update_ = false; + + float speed = 0.0f; + if (this->state) { + speed = static_cast(this->speed) / static_cast(this->speed_count_); + } + if (speed == 0.0f) { // off means idle + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed) + : this->set_hbridge_levels_(speed, speed, speed); + return; + } + if (this->direction == fan::FAN_DIRECTION_FORWARD) { + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f) + : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed) + : this->set_hbridge_levels_(0.0f, 1.0f, speed); + } + } else { // fan::FAN_DIRECTION_REVERSE + if (this->decay_mode_ == DECAY_MODE_SLOW) { + (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed) + : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f); + } else { // DECAY_MODE_FAST + (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f) + : this->set_hbridge_levels_(1.0f, 0.0f, speed); + } + } +} + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h new file mode 100644 index 0000000000..984318c8d6 --- /dev/null +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/output/binary_output.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace hbridge { + +enum DecayMode { + DECAY_MODE_SLOW = 0, + DECAY_MODE_FAST = 1, +}; + +class HBridgeFan : public fan::FanState { + public: + HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} + + void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } + void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } + void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + fan::FanStateCall brake(); + + int get_speed_count() { return this->speed_count_; } + // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping + void internal_update() { this->next_update_ = true; } + + protected: + output::FloatOutput *pin_a_; + output::FloatOutput *pin_b_; + output::FloatOutput *enable_{nullptr}; + output::BinaryOutput *oscillating_{nullptr}; + bool next_update_{true}; + int speed_count_{}; + DecayMode decay_mode_{DECAY_MODE_SLOW}; + + void set_hbridge_levels_(float a_level, float b_level); + void set_hbridge_levels_(float a_level, float b_level, float enable); +}; + +template class BrakeAction : public Action { + public: + explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->brake(); } + + HBridgeFan *parent_; +}; + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light/__init__.py similarity index 94% rename from esphome/components/hbridge/light.py rename to esphome/components/hbridge/light/__init__.py index b4ae45977a..fe5c3e9845 100644 --- a/esphome/components/hbridge/light.py +++ b/esphome/components/hbridge/light/__init__.py @@ -2,8 +2,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import light, output from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B +from .. import hbridge_ns + +CODEOWNERS = ["@DotNetDann"] -hbridge_ns = cg.esphome_ns.namespace("hbridge") HBridgeLightOutput = hbridge_ns.class_( "HBridgeLightOutput", cg.PollingComponent, light.LightOutput ) diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h similarity index 100% rename from esphome/components/hbridge/hbridge_light_output.h rename to esphome/components/hbridge/light/hbridge_light_output.h diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 75590f8572..11e9c8e4d4 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema( 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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 fe1c6008d4..7cd81fec1d 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -6,6 +6,10 @@ from esphome.const import ( CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, + DEVICE_CLASS_AQI, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AQI): sensor.sensor_schema( unit_of_measurement=UNIT_INDEX, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, ).extend( { diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 450a958c56..157e8212bd 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All( } ) .extend(cv.polling_component_schema("1s")) - .extend(spi.spi_device_schema()), + .extend(spi.spi_device_schema(False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 52e8984545..69cb87e539 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_DEFAULT_TRANSITION_LENGTH, CONF_DISABLED_BY_DEFAULT, CONF_EFFECTS, + CONF_FLASH_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, CONF_ID, CONF_INTERNAL, @@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( cv.Optional( CONF_DEFAULT_TRANSITION_LENGTH, default="1s" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_FLASH_TRANSITION_LENGTH, default="0s" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), } ) @@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config): config[CONF_DEFAULT_TRANSITION_LENGTH] ) ) + if CONF_FLASH_TRANSITION_LENGTH in config: + cg.add( + light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH]) + ) if CONF_GAMMA_CORRECT in config: cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT])) effects = await cg.build_registry_list( diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 12eab6a685..a8fa2cd7ac 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -12,14 +12,13 @@ void AddressableLight::call_setup() { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE this->set_interval(5000, [this]() { const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str(); - ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_), - YESNO(this->next_show_)); + ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_)); for (int i = 0; i < this->size(); i++) { auto color = this->get(i); ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), color.get_blue_raw(), color.get_white_raw()); } - ESP_LOGVV(TAG, ""); + ESP_LOGVV(TAG, " "); }); #endif } @@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) { return Color(r, g, b, w); } -void AddressableLight::write_state(LightState *state) { +void AddressableLight::update_state(LightState *state) { auto val = state->current_values; auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); this->correction_.set_local_brightness(max_brightness); @@ -46,9 +45,14 @@ void AddressableLight::write_state(LightState *state) { // don't use LightState helper, gamma correction+brightness is handled by ESPColorView this->all() = esp_color_from_light_color_values(val); + this->schedule_show(); } void AddressableLightTransformer::start() { + // don't try to transition over running effects. + if (this->light_.is_effect_active()) + return; + auto end_values = this->target_values_; this->target_color_ = esp_color_from_light_color_values(end_values); diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index ab1efdf160..bba2158457 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component { amnt = this->size(); this->range(amnt, this->size()) = this->range(0, -amnt); } + // Indicates whether an effect that directly updates the output buffer is active to prevent overwriting 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( @@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component { this->correction_.calculate_gamma_table(state->get_gamma_correct()); this->state_parent_ = state; } - void schedule_show() { this->next_show_ = true; } + void update_state(LightState *state) override; + void schedule_show() { this->state_parent_->next_write_ = true; } #ifdef USE_POWER_SUPPLY void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } @@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component { protected: friend class AddressableLightTransformer; - bool should_show_() const { return this->effect_active_ || this->next_show_; } void mark_shown_() { - this->next_show_ = false; #ifdef USE_POWER_SUPPLY for (auto c : *this) { if (c.get().is_on()) { @@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component { virtual ESPColorView get_view_internal(int32_t index) const = 0; bool effect_active_{false}; - bool next_show_{true}; ESPColorCorrection correction_{}; #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 3a2ba66845..1cb29dfa4e 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; + it.schedule_show(); } } @@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect { var = hsv; hue += add; } + it.schedule_show(); } void set_speed(uint32_t speed) { this->speed_ = speed; } void set_width(uint16_t width) { this->width_ = width; } @@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { new_color.b = c.b; } } + it.schedule_show(); } protected: @@ -151,25 +154,27 @@ 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; + const uint32_t now = millis(); + if (now - this->last_move_ < this->move_interval_) + return; + if (direction_) { + this->at_led_++; + if (this->at_led_ == it.size() - this->scan_width_) + this->direction_ = false; + } else { + this->at_led_--; + if (this->at_led_ == 0) + this->direction_ = true; + } + this->last_move_ = now; + + it.all() = Color::BLACK; for (auto i = 0; i < this->scan_width_; i++) { it[this->at_led_ + i] = current_color; } - const uint32_t now = millis(); - if (now - this->last_move_ > this->move_interval_) { - if (direction_) { - this->at_led_++; - if (this->at_led_ == it.size() - this->scan_width_) - this->direction_ = false; - } else { - this->at_led_--; - if (this->at_led_ == 0) - this->direction_ = true; - } - this->last_move_ = now; - } + it.schedule_show(); } protected: @@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { continue; addressable[pos].set_effect_data(1); } + addressable.schedule_show(); } void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } @@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { const uint8_t color = random_uint32() & 0b111; it[pos].set_effect_data(0b1000 | color); } + it.schedule_show(); } void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } @@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { it[pos] = current_color; } } + it.schedule_show(); } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; } @@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect { // slowly fade back to "real" value var = (var.get() * inv_intensity) + (current_color * intensity); } + it.schedule_show(); } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index f66b90f665..7826b2eecb 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect { 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); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index 0f5b7b4b93..77c377d39e 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -52,7 +52,7 @@ enum class ColorMode : uint8_t { /// Only on/off control. ON_OFF = (uint8_t) ColorCapability::ON_OFF, /// Dimmable light. - BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, + BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | 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. diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 6945d37ded..d979b13368 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -8,26 +8,23 @@ namespace light { static const char *const TAG = "light"; 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 (color_mode == ColorMode::UNKNOWN) + return "Unknown"; + if (color_mode == ColorMode::WHITE) + return "White"; + if (color_mode == ColorMode::COLOR_TEMPERATURE) + return "Color temperature"; + if (color_mode == ColorMode::COLD_WARM_WHITE) + return "Cold/warm white"; + if (color_mode == ColorMode::RGB) + return "RGB"; + if (color_mode == ColorMode::RGB_WHITE) + return "RGBW"; + if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) + return "RGB + cold/warm white"; + if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) + return "RGB + color temperature"; + return ""; } void LightCall::perform() { diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 7568ea6831..73ba0371cd 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -19,6 +19,13 @@ class LightOutput { virtual void setup_state(LightState *state) {} + /// Called on every update of the current values of the associated LightState, + /// can optionally be used to do processing of this change. + virtual void update_state(LightState *state) {} + + /// Called from loop() every time the light state has changed, and should + /// should write the new state to hardware. Every call to write_state() is + /// preceded by (at least) one call to update_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 030cf4b7a2..945d3910d5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -114,9 +114,11 @@ 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()) + if (values.has_value()) { this->current_values = *values; + this->output_->update_state(this); + this->next_write_ = true; + } if (this->transformer_->is_finished()) { this->transformer_->stop(); @@ -127,18 +129,15 @@ void LightState::loop() { // Write state to the light if (this->next_write_) { - this->output_->write_state(this); this->next_write_ = false; + this->output_->write_state(this); } } float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } uint32_t LightState::hash_base() { return 1114400283; } -void LightState::publish_state() { - this->remote_values_callback_.call(); - this->next_write_ = true; -} +void LightState::publish_state() { this->remote_values_callback_.call(); } LightOutput *LightState::get_output() const { return this->output_; } std::string LightState::get_effect_name() { @@ -158,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function &&s void LightState::set_default_transition_length(uint32_t default_transition_length) { this->default_transition_length_ = default_transition_length; } +uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; } +void LightState::set_flash_transition_length(uint32_t flash_transition_length) { + this->flash_transition_length_ = flash_transition_length; +} +uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } bool LightState::supports_effects() { return !this->effects_.empty(); } @@ -235,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) { // 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_target_values(); + end_colors = this->transformer_->get_start_values(); this->transformer_ = make_unique(*this); this->transformer_->setup(end_colors, target, length); @@ -248,6 +252,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot if (set_remote_values) { this->remote_values = target; } + this->output_->update_state(this); this->next_write_ = true; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index dfea9a15f4..dd42aa76db 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -99,6 +99,11 @@ class LightState : public Nameable, public Component { /// 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); + uint32_t get_default_transition_length() const; + + /// Set the flash transition length + void set_flash_transition_length(uint32_t flash_transition_length); + uint32_t get_flash_transition_length() const; /// Set the gamma correction factor void set_gamma_correct(float gamma_correct); @@ -188,6 +193,8 @@ class LightState : public Nameable, public Component { /// Default transition length for all transitions in ms. uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; /// Restore mode of the light. diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index fd0bfd20f3..d501d53f72 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -58,7 +58,43 @@ class LightFlashTransformer : public LightTransformer { public: LightFlashTransformer(LightState &state) : state_(state) {} - optional apply() override { return this->get_target_values(); } + void start() override { + this->transition_length_ = this->state_.get_flash_transition_length(); + if (this->transition_length_ * 2 > this->length_) + this->transition_length_ = this->length_ / 2; + + // do not create transition if length is 0 + if (this->transition_length_ == 0) + return; + + // first transition to original target + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_); + } + + optional apply() override { + // transition transformer does not handle 0 length as progress returns nan + if (this->transition_length_ == 0) + return this->target_values_; + + if (this->transformer_ != nullptr) { + if (!this->transformer_->is_finished()) { + return this->transformer_->apply(); + } else { + this->transformer_->stop(); + this->transformer_ = nullptr; + } + } + + if (millis() > this->start_time_ + this->length_ - this->transition_length_) { + // second transition back to start value + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); + } + + // once transition is complete, don't change states until next transition + return optional(); + } // Restore the original values after the flash. void stop() override { @@ -69,6 +105,8 @@ class LightFlashTransformer : public LightTransformer { protected: LightState &state_; + uint32_t transition_length_; + std::unique_ptr transformer_{nullptr}; }; } // namespace light diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index ce82a51b94..9d79037087 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) { } void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) + if (level > this->level_for(tag) || recursion_guard_) return; + recursion_guard_ = true; this->reset_buffer_(); this->write_header_(level, tag, line); this->vprintf_to_buffer_(format, args); this->write_footer_(); this->log_message_(level, tag); + recursion_guard_ = false; } #ifdef USE_STORE_LOG_STR_IN_FLASH void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) + if (level > this->level_for(tag) || recursion_guard_) return; + recursion_guard_ = true; this->reset_buffer_(); // copy format string const char *format_pgm_p = (PGM_P) format; @@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr this->vprintf_to_buffer_(this->tx_buffer_, args); this->write_footer_(); this->log_message_(level, tag, offset); + recursion_guard_ = false; } #endif diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 1724875229..365261cb91 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -113,6 +113,8 @@ class Logger : public Component { }; std::vector log_levels_; CallbackManager log_callback_{}; + /// Prevents recursive log calls, if true a log message is already being processed. + bool recursion_guard_ = false; }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 019b7c7e64..c22d377b3c 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config): # BEGIN Removed pin schemas below to show error in configuration -# TODO remove in 1.19.0 +# TODO remove in 2022.5.0 for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: PIN_SCHEMA = cv.Schema( @@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: } ) + # pylint: disable=cell-var-from-loop @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA)) def pin_to_code(config): pass diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 1a111f7891..0081f42952 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ID, CONF_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, @@ -34,6 +35,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/midea/__init__.py b/esphome/components/midea/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/adapter.cpp new file mode 100644 index 0000000000..bd5b289095 --- /dev/null +++ b/esphome/components/midea/adapter.cpp @@ -0,0 +1,173 @@ +#include "esphome/core/log.h" +#include "adapter.h" + +namespace esphome { +namespace midea { + +const char *const Constants::TAG = "midea"; +const std::string Constants::FREEZE_PROTECTION = "freeze protection"; +const std::string Constants::SILENT = "silent"; +const std::string Constants::TURBO = "turbo"; + +ClimateMode Converters::to_climate_mode(MideaMode mode) { + switch (mode) { + case MideaMode::MODE_AUTO: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + case MideaMode::MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + case MideaMode::MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + case MideaMode::MODE_FAN_ONLY: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + case MideaMode::MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + default: + return ClimateMode::CLIMATE_MODE_OFF; + } +} + +MideaMode Converters::to_midea_mode(ClimateMode mode) { + switch (mode) { + case ClimateMode::CLIMATE_MODE_HEAT_COOL: + return MideaMode::MODE_AUTO; + case ClimateMode::CLIMATE_MODE_COOL: + return MideaMode::MODE_COOL; + case ClimateMode::CLIMATE_MODE_DRY: + return MideaMode::MODE_DRY; + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + return MideaMode::MODE_FAN_ONLY; + case ClimateMode::CLIMATE_MODE_HEAT: + return MideaMode::MODE_HEAT; + default: + return MideaMode::MODE_OFF; + } +} + +ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) { + switch (mode) { + case MideaSwingMode::SWING_VERTICAL: + return ClimateSwingMode::CLIMATE_SWING_VERTICAL; + case MideaSwingMode::SWING_HORIZONTAL: + return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL; + case MideaSwingMode::SWING_BOTH: + return ClimateSwingMode::CLIMATE_SWING_BOTH; + default: + return ClimateSwingMode::CLIMATE_SWING_OFF; + } +} + +MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) { + switch (mode) { + case ClimateSwingMode::CLIMATE_SWING_VERTICAL: + return MideaSwingMode::SWING_VERTICAL; + case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: + return MideaSwingMode::SWING_HORIZONTAL; + case ClimateSwingMode::CLIMATE_SWING_BOTH: + return MideaSwingMode::SWING_BOTH; + default: + return MideaSwingMode::SWING_OFF; + } +} + +MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) { + switch (mode) { + case ClimateFanMode::CLIMATE_FAN_LOW: + return MideaFanMode::FAN_LOW; + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + return MideaFanMode::FAN_MEDIUM; + case ClimateFanMode::CLIMATE_FAN_HIGH: + return MideaFanMode::FAN_HIGH; + default: + return MideaFanMode::FAN_AUTO; + } +} + +ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + case MideaFanMode::FAN_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + case MideaFanMode::FAN_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + default: + return ClimateFanMode::CLIMATE_FAN_AUTO; + } +} + +bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_SILENT: + case MideaFanMode::FAN_TURBO: + return true; + default: + return false; + } +} + +const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { + switch (mode) { + case MideaFanMode::FAN_SILENT: + return Constants::SILENT; + default: + return Constants::TURBO; + } +} + +MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { + if (mode == Constants::SILENT) + return MideaFanMode::FAN_SILENT; + return MideaFanMode::FAN_TURBO; +} + +MideaPreset Converters::to_midea_preset(ClimatePreset preset) { + switch (preset) { + case ClimatePreset::CLIMATE_PRESET_SLEEP: + return MideaPreset::PRESET_SLEEP; + case ClimatePreset::CLIMATE_PRESET_ECO: + return MideaPreset::PRESET_ECO; + case ClimatePreset::CLIMATE_PRESET_BOOST: + return MideaPreset::PRESET_TURBO; + default: + return MideaPreset::PRESET_NONE; + } +} + +ClimatePreset Converters::to_climate_preset(MideaPreset preset) { + switch (preset) { + case MideaPreset::PRESET_SLEEP: + return ClimatePreset::CLIMATE_PRESET_SLEEP; + case MideaPreset::PRESET_ECO: + return ClimatePreset::CLIMATE_PRESET_ECO; + case MideaPreset::PRESET_TURBO: + return ClimatePreset::CLIMATE_PRESET_BOOST; + default: + return ClimatePreset::CLIMATE_PRESET_NONE; + } +} + +bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } + +const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } + +MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } + +void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { + if (capabilities.supportAutoMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL); + if (capabilities.supportCoolMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL); + if (capabilities.supportHeatMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT); + if (capabilities.supportDryMode()) + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY); + if (capabilities.supportTurboPreset()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); + if (capabilities.supportEcoPreset()) + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); + if (capabilities.supportFrostProtectionPreset()) + traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); +} + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/adapter.h new file mode 100644 index 0000000000..8d8d57e8f9 --- /dev/null +++ b/esphome/components/midea/adapter.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include "esphome/components/climate/climate_traits.h" +#include "appliance_base.h" + +namespace esphome { +namespace midea { + +using MideaMode = dudanov::midea::ac::Mode; +using MideaSwingMode = dudanov::midea::ac::SwingMode; +using MideaFanMode = dudanov::midea::ac::FanMode; +using MideaPreset = dudanov::midea::ac::Preset; + +class Constants { + public: + static const char *const TAG; + static const std::string FREEZE_PROTECTION; + static const std::string SILENT; + static const std::string TURBO; +}; + +class Converters { + public: + static MideaMode to_midea_mode(ClimateMode mode); + static ClimateMode to_climate_mode(MideaMode mode); + static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); + static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); + static MideaPreset to_midea_preset(ClimatePreset preset); + static MideaPreset to_midea_preset(const std::string &preset); + static bool is_custom_midea_preset(MideaPreset preset); + static ClimatePreset to_climate_preset(MideaPreset preset); + static const std::string &to_custom_climate_preset(MideaPreset preset); + static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); + static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); + static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); + static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); + static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); + static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp new file mode 100644 index 0000000000..a71f1dbdfb --- /dev/null +++ b/esphome/components/midea/air_conditioner.cpp @@ -0,0 +1,152 @@ +#include "esphome/core/log.h" +#include "air_conditioner.h" +#include "adapter.h" +#ifdef USE_REMOTE_TRANSMITTER +#include "midea_ir.h" +#endif + +namespace esphome { +namespace midea { + +static void set_sensor(Sensor *sensor, float value) { + if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) + sensor->publish_state(value); +} + +template void update_property(T &property, const T &value, bool &flag) { + if (property != value) { + property = value; + flag = true; + } +} + +void AirConditioner::on_status_change() { + bool need_publish = false; + update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish); + update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish); + auto mode = Converters::to_climate_mode(this->base_.getMode()); + update_property(this->mode, mode, need_publish); + auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode()); + update_property(this->swing_mode, swing_mode, need_publish); + // Preset + auto preset = this->base_.getPreset(); + if (Converters::is_custom_midea_preset(preset)) { + if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset))) + need_publish = true; + } else if (this->set_preset_(Converters::to_climate_preset(preset))) { + need_publish = true; + } + // Fan mode + auto fan_mode = this->base_.getFanMode(); + if (Converters::is_custom_midea_fan_mode(fan_mode)) { + if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode))) + need_publish = true; + } else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) { + need_publish = true; + } + if (need_publish) + this->publish_state(); + set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp()); + set_sensor(this->power_sensor_, this->base_.getPowerUsage()); + set_sensor(this->humidity_sensor_, this->base_.getIndoorHum()); +} + +void AirConditioner::control(const ClimateCall &call) { + dudanov::midea::ac::Control ctrl{}; + if (call.get_target_temperature().has_value()) + ctrl.targetTemp = call.get_target_temperature().value(); + if (call.get_swing_mode().has_value()) + ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value()); + if (call.get_mode().has_value()) + ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); + if (call.get_preset().has_value()) + ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); + else if (call.get_custom_preset().has_value()) + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); + if (call.get_fan_mode().has_value()) + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); + else if (call.get_custom_fan_mode().has_value()) + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); + this->base_.control(ctrl); +} + +ClimateTraits AirConditioner::traits() { + auto traits = ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_visual_min_temperature(17); + traits.set_visual_max_temperature(30); + traits.set_visual_temperature_step(0.5); + traits.set_supported_modes(this->supported_modes_); + traits.set_supported_swing_modes(this->supported_swing_modes_); + traits.set_supported_presets(this->supported_presets_); + traits.set_supported_custom_presets(this->supported_custom_presets_); + traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + /* + MINIMAL SET OF CAPABILITIES */ + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); + traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); + traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) + Converters::to_climate_traits(traits, this->base_.getCapabilities()); + return traits; +} + +void AirConditioner::dump_config() { + ESP_LOGCONFIG(Constants::TAG, "MideaDongle:"); + ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod()); + ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout()); + ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts()); +#ifdef USE_REMOTE_TRANSMITTER + ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter"); +#endif + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) { + this->base_.getCapabilities().dump(); + } else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) { + ESP_LOGW(Constants::TAG, + "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your " + "appliance options."); + } + this->dump_traits_(Constants::TAG); +} + +/* ACTIONS */ + +void AirConditioner::do_follow_me(float temperature, bool beeper) { +#ifdef USE_REMOTE_TRANSMITTER + IrFollowMeData data(static_cast(lroundf(temperature)), beeper); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif +} + +void AirConditioner::do_swing_step() { +#ifdef USE_REMOTE_TRANSMITTER + IrSpecialData data(0x01); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif +} + +void AirConditioner::do_display_toggle() { + if (this->base_.getCapabilities().supportLightControl()) { + this->base_.displayToggle(); + } else { +#ifdef USE_REMOTE_TRANSMITTER + IrSpecialData data(0x08); + this->transmit_ir(data); +#else + ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); +#endif + } +} + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h new file mode 100644 index 0000000000..895b6412f3 --- /dev/null +++ b/esphome/components/midea/air_conditioner.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include "appliance_base.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace midea { + +using sensor::Sensor; +using climate::ClimateCall; + +class AirConditioner : public ApplianceBase { + public: + void dump_config() override; + void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } + void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; } + void on_status_change() override; + + /* ############### */ + /* ### ACTIONS ### */ + /* ############### */ + + void do_follow_me(float temperature, bool beeper = false); + void do_display_toggle(); + void do_swing_step(); + void do_beeper_on() { this->set_beeper_feedback(true); } + void do_beeper_off() { this->set_beeper_feedback(false); } + void do_power_on() { this->base_.setPowerState(true); } + void do_power_off() { this->base_.setPowerState(false); } + + protected: + void control(const ClimateCall &call) override; + ClimateTraits traits() override; + Sensor *outdoor_sensor_{nullptr}; + Sensor *humidity_sensor_{nullptr}; + Sensor *power_sensor_{nullptr}; +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h new file mode 100644 index 0000000000..aa616ced36 --- /dev/null +++ b/esphome/components/midea/appliance_base.h @@ -0,0 +1,76 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/climate/climate.h" +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#endif +#include +#include + +namespace esphome { +namespace midea { + +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; + +template class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate { + static_assert(std::is_base_of::value, + "T must derive from dudanov::midea::ApplianceBase class"); + + public: + ApplianceBase() { + this->base_.setStream(this); + this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); + dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) { + esp_log_vprintf_(level, tag, line, format.c_str(), args); + }); + } + bool can_proceed() override { + return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; + } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + void setup() override { this->base_.setup(); } + void loop() override { this->base_.loop(); } + void set_period(uint32_t ms) { this->base_.setPeriod(ms); } + void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); } + void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); } + void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } + void set_autoconf(bool value) { this->base_.setAutoconf(value); } + void set_supported_modes(std::set modes) { this->supported_modes_ = std::move(modes); } + void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } + void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } + void set_custom_presets(std::set presets) { this->supported_custom_presets_ = std::move(presets); } + void set_custom_fan_modes(std::set modes) { this->supported_custom_fan_modes_ = std::move(modes); } + virtual void on_status_change() = 0; +#ifdef USE_REMOTE_TRANSMITTER + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void transmit_ir(remote_base::MideaData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); + } +#endif + + protected: + T base_; + std::set supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; +#ifdef USE_REMOTE_TRANSMITTER + remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; +#endif +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/automations.h new file mode 100644 index 0000000000..1f026c0c15 --- /dev/null +++ b/esphome/components/midea/automations.h @@ -0,0 +1,56 @@ +#pragma once +#include "esphome/core/automation.h" +#include "air_conditioner.h" + +namespace esphome { +namespace midea { + +template class MideaActionBase : public Action { + public: + void set_parent(AirConditioner *parent) { this->parent_ = parent; } + + protected: + AirConditioner *parent_; +}; + +template class FollowMeAction : public MideaActionBase { + TEMPLATABLE_VALUE(float, temperature) + TEMPLATABLE_VALUE(bool, beeper) + + void play(Ts... x) override { + this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...)); + } +}; + +template class SwingStepAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_swing_step(); } +}; + +template class DisplayToggleAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_display_toggle(); } +}; + +template class BeeperOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_on(); } +}; + +template class BeeperOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_off(); } +}; + +template class PowerOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_on(); } +}; + +template class PowerOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_off(); } +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py new file mode 100644 index 0000000000..137fcdd607 --- /dev/null +++ b/esphome/components/midea/climate.py @@ -0,0 +1,284 @@ +from esphome.core import coroutine +from esphome import automation +from esphome.components import climate, sensor, uart, remote_transmitter +from esphome.components.remote_base import CONF_TRANSMITTER_ID +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_AUTOCONF, + CONF_BEEPER, + CONF_CUSTOM_FAN_MODES, + CONF_CUSTOM_PRESETS, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_PERIOD, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_TIMEOUT, + CONF_TEMPERATURE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + ICON_POWER, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_WATT, +) +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) + +CODEOWNERS = ["@dudanov"] +DEPENDENCIES = ["climate", "uart", "wifi"] +AUTO_LOAD = ["sensor"] +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" +CONF_POWER_USAGE = "power_usage" +CONF_HUMIDITY_SETPOINT = "humidity_setpoint" +midea_ns = cg.esphome_ns.namespace("midea") +AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = midea_ns.namespace("Constants") + + +def templatize(value): + if isinstance(value, cv.Schema): + value = value.schema + ret = {} + for key, val in value.items(): + ret[key] = cv.templatable(val) + return cv.Schema(ret) + + +def register_action(name, type_, schema): + validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA) + registerer = automation.register_action(f"midea_ac.{name}", type_, validator) + + def decorator(func): + async def new_func(config, action_id, template_arg, args): + ac_ = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(ac_)) + await coroutine(func)(var, config, args) + return var + + return registerer(new_func) + + return decorator + + +ALLOWED_CLIMATE_MODES = { + "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, +} + +ALLOWED_CLIMATE_PRESETS = { + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + +ALLOWED_CLIMATE_SWING_MODES = { + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +CUSTOM_FAN_MODES = { + "SILENT": Capabilities.SILENT, + "TURBO": Capabilities.TURBO, +} + +CUSTOM_PRESETS = { + "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION, +} + +validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True) +validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True) +validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) +validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) +validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AirConditioner), + cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, + cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, + cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), + cv.Optional(CONF_TRANSMITTER_ID): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + cv.Optional(CONF_BEEPER, default=False): cv.boolean, + cv.Optional(CONF_AUTOCONF, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( + validate_swing_modes + ), + cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets), + cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( + validate_custom_fan_modes + ), + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + 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_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_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +# Actions +FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) + +MIDEA_ACTION_BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(AirConditioner), + } +) + +# FollowMe action +MIDEA_FOLLOW_ME_MIN = 0 +MIDEA_FOLLOW_ME_MAX = 37 +MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), + } +) + + +@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA) +async def follow_me_to_code(var, config, args): + template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) + cg.add(var.set_beeper(template_)) + template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) + cg.add(var.set_temperature(template_)) + + +# Toggle Display action +@register_action( + "display_toggle", + DisplayToggleAction, + cv.Schema({}), +) +async def display_toggle_to_code(var, config, args): + pass + + +# Swing Step action +@register_action( + "swing_step", + SwingStepAction, + cv.Schema({}), +) +async def swing_step_to_code(var, config, args): + pass + + +# Beeper On action +@register_action( + "beeper_on", + BeeperOnAction, + cv.Schema({}), +) +async def beeper_on_to_code(var, config, args): + pass + + +# Beeper Off action +@register_action( + "beeper_off", + BeeperOffAction, + cv.Schema({}), +) +async def beeper_off_to_code(var, config, args): + pass + + +# Power On action +@register_action( + "power_on", + PowerOnAction, + cv.Schema({}), +) +async def power_on_to_code(var, config, args): + pass + + +# Power Off action +@register_action( + "power_off", + PowerOffAction, + cv.Schema({}), +) +async def power_off_to_code(var, config, args): + pass + + +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) + await climate.register_climate(var, config) + cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) + cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) + cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) + if CONF_TRANSMITTER_ID in config: + cg.add_define("USE_REMOTE_TRANSMITTER") + transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter_)) + cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) + cg.add(var.set_autoconf(config[CONF_AUTOCONF])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_CUSTOM_PRESETS in config: + cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) + if CONF_CUSTOM_FAN_MODES in config: + cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_POWER_USAGE in config: + sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) + cg.add(var.set_power_sensor(sens)) + if CONF_HUMIDITY_SETPOINT in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) + cg.add(var.set_humidity_setpoint_sensor(sens)) + cg.add_library("dudanov/MideaUART", "1.1.5") diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/midea_ir.h new file mode 100644 index 0000000000..2459d844a1 --- /dev/null +++ b/esphome/components/midea/midea_ir.h @@ -0,0 +1,42 @@ +#pragma once +#ifdef USE_REMOTE_TRANSMITTER +#include "esphome/components/remote_base/midea_protocol.h" + +namespace esphome { +namespace midea { + +using IrData = remote_base::MideaData; + +class IrFollowMeData : public IrData { + public: + // Default constructor (temp: 30C, beeper: off) + IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} + // Copy from Base + IrFollowMeData(const IrData &data) : IrData(data) {} + // Direct from temperature and beeper values + IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() { + this->set_temp(temp); + this->set_beeper(beeper); + } + + /* TEMPERATURE */ + uint8_t temp() const { return this->data_[4] - 1; } + void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; } + + /* BEEPER */ + bool beeper() const { return this->data_[3] & 128; } + void set_beeper(bool val) { this->set_value_(3, 1, 7, val); } + + protected: + static const uint8_t MAX_TEMP = 37; +}; + +class IrSpecialData : public IrData { + public: + IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} +}; + +} // namespace midea +} // namespace esphome + +#endif diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py index 741741fd03..f336f84787 100644 --- a/esphome/components/midea_ac/climate.py +++ b/esphome/components/midea_ac/climate.py @@ -1,115 +1,3 @@ -from esphome.components import climate, sensor import esphome.config_validation as cv -import esphome.codegen as cg -from esphome.const import ( - CONF_CUSTOM_FAN_MODES, - CONF_CUSTOM_PRESETS, - CONF_ID, - CONF_PRESET_BOOST, - CONF_PRESET_ECO, - CONF_PRESET_SLEEP, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_PERCENT, - UNIT_WATT, - ICON_THERMOMETER, - ICON_POWER, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - ICON_WATER_PERCENT, - DEVICE_CLASS_HUMIDITY, -) -from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle -AUTO_LOAD = ["climate", "sensor", "midea_dongle"] -CODEOWNERS = ["@dudanov"] -CONF_BEEPER = "beeper" -CONF_SWING_HORIZONTAL = "swing_horizontal" -CONF_SWING_BOTH = "swing_both" -CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" -CONF_POWER_USAGE = "power_usage" -CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ac_ns = cg.esphome_ns.namespace("midea_ac") -MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component) - -CLIMATE_CUSTOM_FAN_MODES = { - "SILENT": "silent", - "TURBO": "turbo", -} - -validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True) - -CLIMATE_CUSTOM_PRESETS = { - "FREEZE_PROTECTION": "freeze protection", -} - -validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True) - -CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MideaAC), - cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle), - cv.Optional(CONF_BEEPER, default=False): cv.boolean, - cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( - validate_climate_custom_fan_mode - ), - cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list( - validate_climate_custom_preset - ), - cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean, - cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean, - cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean, - 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_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_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_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) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await climate.register_climate(var, config) - paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID]) - cg.add(var.set_midea_dongle_parent(paren)) - cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) - if CONF_CUSTOM_FAN_MODES in config: - cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) - if CONF_CUSTOM_PRESETS in config: - cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) - cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL])) - cg.add(var.set_swing_both(config[CONF_SWING_BOTH])) - cg.add(var.set_preset_eco(config[CONF_PRESET_ECO])) - cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP])) - cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST])) - if CONF_OUTDOOR_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) - cg.add(var.set_outdoor_temperature_sensor(sens)) - if CONF_POWER_USAGE in config: - sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) - cg.add(var.set_power_sensor(sens)) - if CONF_HUMIDITY_SETPOINT in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) - cg.add(var.set_humidity_setpoint_sensor(sens)) +CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9") diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp deleted file mode 100644 index 72f7d23404..0000000000 --- a/esphome/components/midea_ac/midea_climate.cpp +++ /dev/null @@ -1,208 +0,0 @@ -#include "esphome/core/log.h" -#include "midea_climate.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; - -static void set_sensor(sensor::Sensor *sensor, float value) { - if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) - sensor->publish_state(value); -} - -template void set_property(T &property, T value, bool &flag) { - if (property != value) { - property = value; - flag = true; - } -} - -void MideaAC::on_frame(const midea_dongle::Frame &frame) { - const auto p = frame.as(); - if (p.has_power_info()) { - set_sensor(this->power_sensor_, p.get_power_usage()); - return; - } else if (!p.has_properties()) { - ESP_LOGW(TAG, "RX: frame has unknown type"); - return; - } - if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) { - ESP_LOGD(TAG, "RX: control frame"); - this->ctrl_request_ = false; - } else { - ESP_LOGD(TAG, "RX: query frame"); - } - if (this->ctrl_request_) - return; - this->cmd_frame_.set_properties(p); // copy properties from response - bool need_publish = false; - set_property(this->mode, p.get_mode(), need_publish); - set_property(this->target_temperature, p.get_target_temp(), need_publish); - set_property(this->current_temperature, p.get_indoor_temp(), need_publish); - if (p.is_custom_fan_mode()) { - this->fan_mode.reset(); - optional mode = p.get_custom_fan_mode(); - set_property(this->custom_fan_mode, mode, need_publish); - } else { - this->custom_fan_mode.reset(); - optional mode = p.get_fan_mode(); - set_property(this->fan_mode, mode, need_publish); - } - set_property(this->swing_mode, p.get_swing_mode(), need_publish); - if (p.is_custom_preset()) { - this->preset.reset(); - optional preset = p.get_custom_preset(); - set_property(this->custom_preset, preset, need_publish); - } else { - this->custom_preset.reset(); - set_property(this->preset, p.get_preset(), need_publish); - } - if (need_publish) - this->publish_state(); - set_sensor(this->outdoor_sensor_, p.get_outdoor_temp()); - set_sensor(this->humidity_sensor_, p.get_humidity_setpoint()); -} - -void MideaAC::on_update() { - if (this->ctrl_request_) { - ESP_LOGD(TAG, "TX: control"); - this->parent_->write_frame(this->cmd_frame_); - } else { - ESP_LOGD(TAG, "TX: query"); - if (this->power_sensor_ == nullptr || this->request_num_++ % 32) - this->parent_->write_frame(this->query_frame_); - else - this->parent_->write_frame(this->power_frame_); - } -} - -bool MideaAC::allow_preset(climate::ClimatePreset preset) const { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - if (this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "ECO preset is only available in COOL mode"); - } - break; - case climate::CLIMATE_PRESET_SLEEP: - if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) { - ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode"); - } else { - return true; - } - break; - case climate::CLIMATE_PRESET_BOOST: - if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode"); - } - break; - case climate::CLIMATE_PRESET_NONE: - return true; - default: - break; - } - return false; -} - -bool MideaAC::allow_custom_preset(const std::string &custom_preset) const { - if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) { - if (this->mode == climate::CLIMATE_MODE_HEAT) { - return true; - } else { - ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str()); - } - } - return false; -} - -void MideaAC::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value() && call.get_mode().value() != this->mode) { - this->cmd_frame_.set_mode(call.get_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) { - this->cmd_frame_.set_target_temp(call.get_target_temperature().value()); - this->ctrl_request_ = true; - } - if (call.get_fan_mode().has_value() && - (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) { - this->custom_fan_mode.reset(); - this->cmd_frame_.set_fan_mode(call.get_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_fan_mode().has_value() && - (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) { - this->fan_mode.reset(); - this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) { - this->cmd_frame_.set_swing_mode(call.get_swing_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) && - (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) { - this->custom_preset.reset(); - this->cmd_frame_.set_preset(call.get_preset().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) && - (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) { - this->preset.reset(); - this->cmd_frame_.set_custom_preset(call.get_custom_preset().value()); - this->ctrl_request_ = true; - } - if (this->ctrl_request_) { - this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_); - this->cmd_frame_.finalize(); - } -} - -climate::ClimateTraits MideaAC::traits() { - auto traits = climate::ClimateTraits(); - traits.set_visual_min_temperature(17); - traits.set_visual_max_temperature(30); - traits.set_visual_temperature_step(0.5); - traits.set_supported_modes({ - climate::CLIMATE_MODE_OFF, - climate::CLIMATE_MODE_HEAT_COOL, - climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_DRY, - climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_FAN_ONLY, - }); - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, - }); - traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_); - traits.set_supported_swing_modes({ - climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL, - }); - if (traits_swing_horizontal_) - traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); - if (traits_swing_both_) - traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); - traits.set_supported_presets({ - climate::CLIMATE_PRESET_NONE, - }); - if (traits_preset_eco_) - traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); - if (traits_preset_sleep_) - traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); - if (traits_preset_boost_) - traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST); - traits.set_supported_custom_presets(this->traits_custom_presets_); - traits.set_supports_current_temperature(true); - return traits; -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h deleted file mode 100644 index 62bd4c339e..0000000000 --- a/esphome/components/midea_ac/midea_climate.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/core/component.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component { - public: - float get_setup_priority() const override { return setup_priority::LATE; } - void on_frame(const midea_dongle::Frame &frame) override; - void on_update() override; - void setup() override { this->parent_->set_appliance(this); } - void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; } - void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } - void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } - void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } - void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; } - void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; } - void set_swing_both(bool state) { this->traits_swing_both_ = state; } - void set_preset_eco(bool state) { this->traits_preset_eco_ = state; } - void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; } - void set_preset_boost(bool state) { this->traits_preset_boost_ = state; } - bool allow_preset(climate::ClimatePreset preset) const; - void set_custom_fan_modes(std::set custom_fan_modes) { - this->traits_custom_fan_modes_ = std::move(custom_fan_modes); - } - void set_custom_presets(std::set custom_presets) { - this->traits_custom_presets_ = std::move(custom_presets); - } - bool allow_custom_preset(const std::string &custom_preset) const; - - protected: - /// Override control to change settings of the climate device. - void control(const climate::ClimateCall &call) override; - /// Return the traits of this controller. - climate::ClimateTraits traits() override; - - const QueryFrame query_frame_; - const PowerQueryFrame power_frame_; - CommandFrame cmd_frame_; - midea_dongle::MideaDongle *parent_{nullptr}; - sensor::Sensor *outdoor_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *power_sensor_{nullptr}; - uint8_t request_num_{0}; - bool ctrl_request_{false}; - bool beeper_feedback_{false}; - bool traits_swing_horizontal_{false}; - bool traits_swing_both_{false}; - bool traits_preset_eco_{false}; - bool traits_preset_sleep_{false}; - bool traits_preset_boost_{false}; - std::set traits_custom_fan_modes_{{}}; - std::set traits_custom_presets_{{}}; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp deleted file mode 100644 index c0a5ce4b55..0000000000 --- a/esphome/components/midea_ac/midea_frame.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; -const std::string MIDEA_SILENT_FAN_MODE = "silent"; -const std::string MIDEA_TURBO_FAN_MODE = "turbo"; -const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; - -const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81, - 0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31}; - -const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, - 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A}; - -const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00, - 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -float PropertiesFrame::get_target_temp() const { - float temp = static_cast((this->pbuf_[12] & 0x0F) + 16); - if (this->pbuf_[12] & 0x10) - temp += 0.5; - return temp; -} - -void PropertiesFrame::set_target_temp(float temp) { - uint8_t tmp = static_cast(temp * 16.0) + 4; - tmp = ((tmp & 8) << 1) | (tmp >> 4); - this->pbuf_[12] &= ~0x1F; - this->pbuf_[12] |= tmp; -} - -static float i16tof(int16_t in) { return static_cast(in - 50) / 2.0; } -float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); } -float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); } -float PropertiesFrame::get_humidity_setpoint() const { return static_cast(this->pbuf_[29] & 0x7F); } - -climate::ClimateMode PropertiesFrame::get_mode() const { - if (!this->get_power_()) - return climate::CLIMATE_MODE_OFF; - switch (this->pbuf_[12] >> 5) { - case MIDEA_MODE_AUTO: - return climate::CLIMATE_MODE_HEAT_COOL; - case MIDEA_MODE_COOL: - return climate::CLIMATE_MODE_COOL; - case MIDEA_MODE_DRY: - return climate::CLIMATE_MODE_DRY; - case MIDEA_MODE_HEAT: - return climate::CLIMATE_MODE_HEAT; - case MIDEA_MODE_FAN_ONLY: - return climate::CLIMATE_MODE_FAN_ONLY; - default: - return climate::CLIMATE_MODE_OFF; - } -} - -void PropertiesFrame::set_mode(climate::ClimateMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_MODE_HEAT_COOL: - m = MIDEA_MODE_AUTO; - break; - case climate::CLIMATE_MODE_COOL: - m = MIDEA_MODE_COOL; - break; - case climate::CLIMATE_MODE_DRY: - m = MIDEA_MODE_DRY; - break; - case climate::CLIMATE_MODE_HEAT: - m = MIDEA_MODE_HEAT; - break; - case climate::CLIMATE_MODE_FAN_ONLY: - m = MIDEA_MODE_FAN_ONLY; - break; - default: - this->set_power_(false); - return; - } - this->set_power_(true); - this->pbuf_[12] &= ~0xE0; - this->pbuf_[12] |= m << 5; -} - -optional PropertiesFrame::get_preset() const { - if (this->get_eco_mode()) - return climate::CLIMATE_PRESET_ECO; - if (this->get_sleep_mode()) - return climate::CLIMATE_PRESET_SLEEP; - if (this->get_turbo_mode()) - return climate::CLIMATE_PRESET_BOOST; - return climate::CLIMATE_PRESET_NONE; -} - -void PropertiesFrame::set_preset(climate::ClimatePreset preset) { - this->clear_presets(); - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - this->set_eco_mode(true); - break; - case climate::CLIMATE_PRESET_SLEEP: - this->set_sleep_mode(true); - break; - case climate::CLIMATE_PRESET_BOOST: - this->set_turbo_mode(true); - break; - default: - break; - } -} - -void PropertiesFrame::clear_presets() { - this->set_eco_mode(false); - this->set_sleep_mode(false); - this->set_turbo_mode(false); - this->set_freeze_protection_mode(false); -} - -bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); } - -const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; }; - -void PropertiesFrame::set_custom_preset(const std::string &preset) { - this->clear_presets(); - if (preset == MIDEA_FREEZE_PROTECTION_PRESET) - this->set_freeze_protection_mode(true); -} - -bool PropertiesFrame::is_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - case MIDEA_FAN_TURBO: - return true; - default: - return false; - } -} - -climate::ClimateFanMode PropertiesFrame::get_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_LOW: - return climate::CLIMATE_FAN_LOW; - case MIDEA_FAN_MEDIUM: - return climate::CLIMATE_FAN_MEDIUM; - case MIDEA_FAN_HIGH: - return climate::CLIMATE_FAN_HIGH; - default: - return climate::CLIMATE_FAN_AUTO; - } -} - -void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_FAN_LOW: - m = MIDEA_FAN_LOW; - break; - case climate::CLIMATE_FAN_MEDIUM: - m = MIDEA_FAN_MEDIUM; - break; - case climate::CLIMATE_FAN_HIGH: - m = MIDEA_FAN_HIGH; - break; - default: - m = MIDEA_FAN_AUTO; - break; - } - this->pbuf_[13] = m; -} - -const std::string &PropertiesFrame::get_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - return MIDEA_SILENT_FAN_MODE; - default: - return MIDEA_TURBO_FAN_MODE; - } -} - -void PropertiesFrame::set_custom_fan_mode(const std::string &mode) { - uint8_t m; - if (mode == MIDEA_SILENT_FAN_MODE) { - m = MIDEA_FAN_SILENT; - } else { - m = MIDEA_FAN_TURBO; - } - this->pbuf_[13] = m; -} - -climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const { - switch (this->pbuf_[17] & 0x0F) { - case MIDEA_SWING_VERTICAL: - return climate::CLIMATE_SWING_VERTICAL; - case MIDEA_SWING_HORIZONTAL: - return climate::CLIMATE_SWING_HORIZONTAL; - case MIDEA_SWING_BOTH: - return climate::CLIMATE_SWING_BOTH; - default: - return climate::CLIMATE_SWING_OFF; - } -} - -void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_SWING_VERTICAL: - m = MIDEA_SWING_VERTICAL; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - m = MIDEA_SWING_HORIZONTAL; - break; - case climate::CLIMATE_SWING_BOTH: - m = MIDEA_SWING_BOTH; - break; - default: - m = MIDEA_SWING_OFF; - break; - } - this->pbuf_[17] = 0x30 | m; -} - -float PropertiesFrame::get_power_usage() const { - uint32_t power = 0; - const uint8_t *ptr = this->pbuf_ + 28; - for (uint32_t weight = 1;; weight *= 10, ptr--) { - power += (*ptr % 16) * weight; - weight *= 10; - power += (*ptr / 16) * weight; - if (weight == 100000) - return static_cast(power) * 0.1; - } -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h deleted file mode 100644 index e1d6fed49d..0000000000 --- a/esphome/components/midea_ac/midea_frame.h +++ /dev/null @@ -1,165 +0,0 @@ -#pragma once -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_frame.h" - -namespace esphome { -namespace midea_ac { - -extern const std::string MIDEA_SILENT_FAN_MODE; -extern const std::string MIDEA_TURBO_FAN_MODE; -extern const std::string MIDEA_FREEZE_PROTECTION_PRESET; - -/// Enum for all modes a Midea device can be in. -enum MideaMode : uint8_t { - /// The Midea device is set to automatically change the heating/cooling cycle - MIDEA_MODE_AUTO = 1, - /// The Midea device is manually set to cool mode (not in auto mode!) - MIDEA_MODE_COOL = 2, - /// The Midea device is manually set to dry mode - MIDEA_MODE_DRY = 3, - /// The Midea device is manually set to heat mode (not in auto mode!) - MIDEA_MODE_HEAT = 4, - /// The Midea device is manually set to fan only mode - MIDEA_MODE_FAN_ONLY = 5, -}; - -/// Enum for all modes a Midea fan can be in -enum MideaFanMode : uint8_t { - /// The fan mode is set to Auto - MIDEA_FAN_AUTO = 102, - /// The fan mode is set to Silent - MIDEA_FAN_SILENT = 20, - /// The fan mode is set to Low - MIDEA_FAN_LOW = 40, - /// The fan mode is set to Medium - MIDEA_FAN_MEDIUM = 60, - /// The fan mode is set to High - MIDEA_FAN_HIGH = 80, - /// The fan mode is set to Turbo - MIDEA_FAN_TURBO = 100, -}; - -/// Enum for all modes a Midea swing can be in -enum MideaSwingMode : uint8_t { - /// The sing mode is set to Off - MIDEA_SWING_OFF = 0b0000, - /// The fan mode is set to Both - MIDEA_SWING_BOTH = 0b1111, - /// The fan mode is set to Vertical - MIDEA_SWING_VERTICAL = 0b1100, - /// The fan mode is set to Horizontal - MIDEA_SWING_HORIZONTAL = 0b0011, -}; - -class PropertiesFrame : public midea_dongle::BaseFrame { - public: - PropertiesFrame() = delete; - PropertiesFrame(uint8_t *data) : BaseFrame(data) {} - PropertiesFrame(const Frame &frame) : BaseFrame(frame) {} - - bool has_properties() const { - return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02)); - } - - bool has_power_info() const { return this->has_response_type(0xC1); } - - /* TARGET TEMPERATURE */ - - float get_target_temp() const; - void set_target_temp(float temp); - - /* MODE */ - climate::ClimateMode get_mode() const; - void set_mode(climate::ClimateMode mode); - - /* FAN SPEED */ - bool is_custom_fan_mode() const; - climate::ClimateFanMode get_fan_mode() const; - void set_fan_mode(climate::ClimateFanMode mode); - - const std::string &get_custom_fan_mode() const; - void set_custom_fan_mode(const std::string &mode); - - /* SWING MODE */ - climate::ClimateSwingMode get_swing_mode() const; - void set_swing_mode(climate::ClimateSwingMode mode); - - /* INDOOR TEMPERATURE */ - float get_indoor_temp() const; - - /* OUTDOOR TEMPERATURE */ - float get_outdoor_temp() const; - - /* HUMIDITY SETPOINT */ - float get_humidity_setpoint() const; - - /* ECO MODE */ - bool get_eco_mode() const { return this->pbuf_[19] & 0x10; } - void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); } - - /* SLEEP MODE */ - bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; } - void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); } - - /* TURBO MODE */ - bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; } - void set_turbo_mode(bool state) { - this->set_bytemask_(18, 0x20, state); - this->set_bytemask_(20, 0x02, state); - } - - /* FREEZE PROTECTION */ - bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; } - void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); } - - /* PRESET */ - optional get_preset() const; - void set_preset(climate::ClimatePreset preset); - void clear_presets(); - - bool is_custom_preset() const; - const std::string &get_custom_preset() const; - void set_custom_preset(const std::string &preset); - - /* POWER USAGE */ - float get_power_usage() const; - - /// Set properties from another frame - void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); } - - protected: - /* POWER */ - bool get_power_() const { return this->pbuf_[11] & 0x01; } - void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); } -}; - -// Query state frame (read-only) -class QueryFrame : public midea_dongle::StaticFrame { - public: - QueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Power query state frame (read-only) -class PowerQueryFrame : public midea_dongle::StaticFrame { - public: - PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Command frame -class CommandFrame : public midea_dongle::StaticFrame { - public: - CommandFrame() : StaticFrame(FPSTR(this->INIT)) {} - void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_dongle/__init__.py b/esphome/components/midea_dongle/__init__.py deleted file mode 100644 index daa8ea6657..0000000000 --- a/esphome/components/midea_dongle/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import uart -from esphome.const import CONF_ID - -DEPENDENCIES = ["wifi", "uart"] -CODEOWNERS = ["@dudanov"] - -midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle") -MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice) - -CONF_MIDEA_DONGLE_ID = "midea_dongle_id" -CONF_STRENGTH_ICON = "strength_icon" -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MideaDongle), - cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean, - } - ) - .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) - cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON])) diff --git a/esphome/components/midea_dongle/midea_dongle.cpp b/esphome/components/midea_dongle/midea_dongle.cpp deleted file mode 100644 index 7e3683a964..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "midea_dongle.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace midea_dongle { - -static const char *const TAG = "midea_dongle"; - -void MideaDongle::loop() { - while (this->available()) { - const uint8_t rx = this->read(); - if (this->idx_ <= OFFSET_LENGTH) { - if (this->idx_ == OFFSET_LENGTH) { - if (rx <= OFFSET_BODY || rx >= sizeof(this->buf_)) { - this->reset_(); - continue; - } - this->cnt_ = rx; - } else if (rx != SYNC_BYTE) { - continue; - } - } - this->buf_[this->idx_++] = rx; - if (--this->cnt_) - continue; - this->reset_(); - const BaseFrame frame(this->buf_); - ESP_LOGD(TAG, "RX: %s", frame.to_string().c_str()); - if (!frame.is_valid()) { - ESP_LOGW(TAG, "RX: frame check failed!"); - continue; - } - if (frame.get_type() == QUERY_NETWORK) { - this->notify_.set_type(QUERY_NETWORK); - this->need_notify_ = true; - continue; - } - if (this->appliance_ != nullptr) - this->appliance_->on_frame(frame); - } -} - -void MideaDongle::update() { - const bool is_conn = WiFi.isConnected(); - uint8_t wifi_strength = 0; - if (!this->rssi_timer_) { - if (is_conn) - wifi_strength = 4; - } else if (is_conn) { - if (--this->rssi_timer_) { - wifi_strength = this->notify_.get_signal_strength(); - } else { - this->rssi_timer_ = 60; - const int32_t dbm = WiFi.RSSI(); - if (dbm > -63) - wifi_strength = 4; - else if (dbm > -75) - wifi_strength = 3; - else if (dbm > -88) - wifi_strength = 2; - else if (dbm > -100) - wifi_strength = 1; - } - } else { - this->rssi_timer_ = 1; - } - if (this->notify_.is_connected() != is_conn) { - this->notify_.set_connected(is_conn); - this->need_notify_ = true; - } - if (this->notify_.get_signal_strength() != wifi_strength) { - this->notify_.set_signal_strength(wifi_strength); - this->need_notify_ = true; - } - if (!--this->notify_timer_) { - this->notify_.set_type(NETWORK_NOTIFY); - this->need_notify_ = true; - } - if (this->need_notify_) { - ESP_LOGD(TAG, "TX: notify WiFi STA %s, signal strength %d", is_conn ? "connected" : "not connected", wifi_strength); - this->need_notify_ = false; - this->notify_timer_ = 600; - this->notify_.finalize(); - this->write_frame(this->notify_); - return; - } - if (this->appliance_ != nullptr) - this->appliance_->on_update(); -} - -void MideaDongle::write_frame(const Frame &frame) { - this->write_array(frame.data(), frame.size()); - ESP_LOGD(TAG, "TX: %s", frame.to_string().c_str()); -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_dongle.h b/esphome/components/midea_dongle/midea_dongle.h deleted file mode 100644 index a7dfb9cf25..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/wifi/wifi_component.h" -#include "esphome/components/uart/uart.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -enum MideaApplianceType : uint8_t { DEHUMIDIFIER = 0xA1, AIR_CONDITIONER = 0xAC, BROADCAST = 0xFF }; -enum MideaMessageType : uint8_t { - DEVICE_CONTROL = 0x02, - DEVICE_QUERY = 0x03, - NETWORK_NOTIFY = 0x0D, - QUERY_NETWORK = 0x63, -}; - -struct MideaAppliance { - /// Calling on update event - virtual void on_update() = 0; - /// Calling on frame receive event - virtual void on_frame(const Frame &frame) = 0; -}; - -class MideaDongle : public PollingComponent, public uart::UARTDevice { - public: - MideaDongle() : PollingComponent(1000) {} - float get_setup_priority() const override { return setup_priority::LATE; } - void update() override; - void loop() override; - void set_appliance(MideaAppliance *app) { this->appliance_ = app; } - void use_strength_icon(bool state) { this->rssi_timer_ = state; } - void write_frame(const Frame &frame); - - protected: - MideaAppliance *appliance_{nullptr}; - NotifyFrame notify_; - unsigned notify_timer_{1}; - // Buffer - uint8_t buf_[36]; - // Index - uint8_t idx_{0}; - // Reverse receive counter - uint8_t cnt_{2}; - uint8_t rssi_timer_{0}; - bool need_notify_{false}; - - // Reset receiver state - void reset_() { - this->idx_ = 0; - this->cnt_ = 2; - } -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.cpp b/esphome/components/midea_dongle/midea_frame.cpp deleted file mode 100644 index acb3feee5f..0000000000 --- a/esphome/components/midea_dongle/midea_frame.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -const uint8_t BaseFrame::CRC_TABLE[] = { - 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 0x9D, 0xC3, 0x21, - 0x7F, 0xFC, 0xA2, 0x40, 0x1E, 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, - 0xFE, 0xA0, 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 0x7C, - 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 0x84, 0xDA, 0x38, 0x66, - 0xE5, 0xBB, 0x59, 0x07, 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, - 0x9A, 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 0xF8, 0xA6, - 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 0x8C, 0xD2, 0x30, 0x6E, 0xED, - 0xB3, 0x51, 0x0F, 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92, - 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 0x6D, 0x33, 0xD1, - 0x8F, 0x0C, 0x52, 0xB0, 0xEE, 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, - 0x2D, 0x73, 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 0x57, - 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 0xE9, 0xB7, 0x55, 0x0B, - 0x88, 0xD6, 0x34, 0x6A, 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, - 0xF7, 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35}; - -const uint8_t NotifyFrame::INIT[] = {0xAA, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0D, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -bool BaseFrame::is_valid() const { return /*this->has_valid_crc_() &&*/ this->has_valid_cs_(); } - -void BaseFrame::finalize() { - this->update_crc_(); - this->update_cs_(); -} - -void BaseFrame::update_crc_() { - uint8_t crc = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - uint8_t len = this->length_() - OFFSET_BODY; - while (--len) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr++)); - *ptr = crc; -} - -void BaseFrame::update_cs_() { - uint8_t cs = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - uint8_t len = this->length_(); - while (--len) - cs -= *ptr++; - *ptr = cs; -} - -bool BaseFrame::has_valid_crc_() const { - uint8_t crc = 0; - uint8_t len = this->length_() - OFFSET_BODY; - const uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - for (; len; ptr++, len--) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr)); - return !crc; -} - -bool BaseFrame::has_valid_cs_() const { - uint8_t cs = 0; - uint8_t len = this->length_(); - const uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - for (; len; ptr++, len--) - cs -= *ptr; - return !cs; -} - -void BaseFrame::set_bytemask_(uint8_t idx, uint8_t mask, bool state) { - uint8_t *dst = this->pbuf_ + idx; - if (state) - *dst |= mask; - else - *dst &= ~mask; -} - -static char u4hex(uint8_t num) { return num + ((num < 10) ? '0' : ('A' - 10)); } - -String Frame::to_string() const { - String ret; - char buf[4]; - buf[2] = ' '; - buf[3] = '\0'; - ret.reserve(3 * 36); - const uint8_t *it = this->data(); - for (size_t i = 0; i < this->size(); i++, it++) { - buf[0] = u4hex(*it >> 4); - buf[1] = u4hex(*it & 15); - ret.concat(buf); - } - return ret; -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.h b/esphome/components/midea_dongle/midea_frame.h deleted file mode 100644 index ce89cc636e..0000000000 --- a/esphome/components/midea_dongle/midea_frame.h +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once -#include "esphome/core/component.h" - -namespace esphome { -namespace midea_dongle { - -static const uint8_t OFFSET_START = 0; -static const uint8_t OFFSET_LENGTH = 1; -static const uint8_t OFFSET_APPTYPE = 2; -static const uint8_t OFFSET_BODY = 10; -static const uint8_t SYNC_BYTE = 0xAA; - -class Frame { - public: - Frame() = delete; - Frame(uint8_t *data) : pbuf_(data) {} - Frame(const Frame &frame) : pbuf_(frame.data()) {} - - // Frame buffer - uint8_t *data() const { return this->pbuf_; } - // Frame size - uint8_t size() const { return this->length_() + OFFSET_LENGTH; } - uint8_t app_type() const { return this->pbuf_[OFFSET_APPTYPE]; } - - template typename std::enable_if::value, T>::type as() const { - return T(*this); - } - String to_string() const; - - protected: - uint8_t *pbuf_; - uint8_t length_() const { return this->pbuf_[OFFSET_LENGTH]; } -}; - -class BaseFrame : public Frame { - public: - BaseFrame() = delete; - BaseFrame(uint8_t *data) : Frame(data) {} - BaseFrame(const Frame &frame) : Frame(frame) {} - - // Check for valid - bool is_valid() const; - // Prepare for sending to device - void finalize(); - uint8_t get_type() const { return this->pbuf_[9]; } - void set_type(uint8_t value) { this->pbuf_[9] = value; } - bool has_response_type(uint8_t type) const { return this->resp_type_() == type; } - bool has_type(uint8_t type) const { return this->get_type() == type; } - - protected: - static const uint8_t PROGMEM CRC_TABLE[256]; - void set_bytemask_(uint8_t idx, uint8_t mask, bool state); - uint8_t resp_type_() const { return this->pbuf_[OFFSET_BODY]; } - bool has_valid_crc_() const; - bool has_valid_cs_() const; - void update_crc_(); - void update_cs_(); -}; - -template class StaticFrame : public T { - public: - // Default constructor - StaticFrame() : T(this->buf_) {} - // Copy constructor - StaticFrame(const Frame &src) : T(this->buf_) { - if (src.length_() < sizeof(this->buf_)) { - memcpy(this->buf_, src.data(), src.length_() + OFFSET_LENGTH); - } - } - // Constructor for RAM data - StaticFrame(const uint8_t *src) : T(this->buf_) { - const uint8_t len = src[OFFSET_LENGTH]; - if (len < sizeof(this->buf_)) { - memcpy(this->buf_, src, len + OFFSET_LENGTH); - } - } - // Constructor for PROGMEM data - StaticFrame(const __FlashStringHelper *pgm) : T(this->buf_) { - const uint8_t *src = reinterpret_cast(pgm); - const uint8_t len = pgm_read_byte(src + OFFSET_LENGTH); - if (len < sizeof(this->buf_)) { - memcpy_P(this->buf_, src, len + OFFSET_LENGTH); - } - } - - protected: - uint8_t buf_[buf_size]; -}; - -// Device network notification frame -class NotifyFrame : public midea_dongle::StaticFrame { - public: - NotifyFrame() : StaticFrame(FPSTR(NotifyFrame::INIT)) {} - void set_signal_strength(uint8_t value) { this->pbuf_[12] = value; } - uint8_t get_signal_strength() const { return this->pbuf_[12]; } - void set_connected(bool state) { this->pbuf_[18] = state ? 0 : 1; } - bool is_connected() const { return !this->pbuf_[18]; } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 5809b6616c..be9dbb0a08 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -60,6 +60,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC root["max_temp"] = traits.get_visual_max_temperature(); // temp_step root["temp_step"] = traits.get_visual_temperature_step(); + // temperature units are always coerced to Celsius internally + root["temp_unit"] = "C"; if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { // away_mode_command_topic diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 13acdcacd8..6ddc080b53 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -102,9 +102,7 @@ bool MQTTComponent::send_discovery_() { device_info["identifiers"] = get_mac_address(); device_info["name"] = node_name; device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); -#ifdef ARDUINO_BOARD - device_info["model"] = ARDUINO_BOARD; -#endif + device_info["model"] = ESPHOME_BOARD; device_info["manufacturer"] = "espressif"; }, 0, discovery_info.retain); diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 4171dae04c..ba9121bc5d 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -65,7 +65,9 @@ void MQTTFanComponent::setup() { if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { - this->state_->make_call().set_speed(payload.c_str()).perform(); + this->state_->make_call() + .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) + .perform(); }); } @@ -99,16 +101,16 @@ bool MQTTFanComponent::publish_state() { if (traits.supports_speed()) { const char *payload; switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { - case FAN_SPEED_LOW: { + case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "low"; break; } - case FAN_SPEED_MEDIUM: { + case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "medium"; break; } default: - case FAN_SPEED_HIGH: { + case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations) payload = "high"; break; } diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index be662867cf..f53be9c010 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -38,28 +38,27 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover root["color_mode"] = true; JsonArray &color_modes = root.createNestedArray("supported_color_modes"); - if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE)) + if (traits.supports_color_mode(ColorMode::ON_OFF)) + color_modes.add("onoff"); + if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) + color_modes.add("brightness"); + if (traits.supports_color_mode(ColorMode::WHITE)) + color_modes.add("white"); + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || + traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) color_modes.add("color_temp"); if (traits.supports_color_mode(ColorMode::RGB)) color_modes.add("rgb"); - if (traits.supports_color_mode(ColorMode::RGB_WHITE)) + if (traits.supports_color_mode(ColorMode::RGB_WHITE) || + // HA doesn't support RGBCT, and there's no CWWW->CT emulation in ESPHome yet, so ignore CT control for now + traits.supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE)) 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.supports_color_capability(ColorCapability::RGB)) - root["rgb"] = true; - if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) - root["color_temp"] = true; - if (traits.supports_color_capability(ColorCapability::WHITE)) - root["white_value"] = true; if (this->state_->supports_effects()) { root["effect"] = true; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index ce7e89c584..d440e30fc4 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -61,8 +61,8 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo if (this->sensor_->get_force_update()) root["force_update"] = true; - if (this->sensor_->state_class == sensor::STATE_CLASS_MEASUREMENT) - root["state_class"] = "measurement"; + if (this->sensor_->state_class != STATE_CLASS_NONE) + root["state_class"] = state_class_to_string(this->sensor_->state_class); config.command_topic = false; } diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index c7f7badc5a..6fa3fb3cd9 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -1,13 +1,14 @@ #pragma once +#include "esphome/core/macros.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/color.h" #include "esphome/components/light/light_output.h" #include "esphome/components/light/addressable_light.h" -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 -#error The NeoPixelBus library requires at least arduino_core_version 2.4.x +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) +#error The NeoPixelBus library requires at least arduino_version 2.4.x #endif #include "NeoPixelBus.h" @@ -82,10 +83,7 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight { this->controller_->Begin(); } - void loop() override { - if (!this->should_show_()) - return; - + void write_state(light::LightState *state) override { this->mark_shown_(); this->controller_->Dirty(); diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 71f8101704..ac72befb9e 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -178,28 +178,29 @@ void OTAComponent::handle_() { #endif if (!Update.begin(ota_size, U_FLASH)) { + uint8_t error = Update.getError(); StreamString ss; Update.printError(ss); #ifdef ARDUINO_ARCH_ESP8266 - if (ss.indexOf("Invalid bootstrapping") != -1) { + if (error == UPDATE_ERROR_BOOTSTRAP) { error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; goto error; } - if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) { + if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) { error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; goto error; } - if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) { + if (error == UPDATE_ERROR_FLASH_CONFIG) { error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; goto error; } - if (ss.indexOf("Not Enough Space") != -1) { + if (error == UPDATE_ERROR_SPACE) { error_code = OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; goto error; } #endif #ifdef ARDUINO_ARCH_ESP32 - if (ss.indexOf("Bad Size Given") != -1) { + if (error == UPDATE_ERROR_SIZE) { error_code = OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; goto error; } diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 8c5c9a0144..330ffc2bf2 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,6 +1,19 @@ +import re +from pathlib import Path +from esphome.core import EsphomeError + +from esphome import git, yaml_util +from esphome.const import ( + CONF_FILE, + CONF_FILES, + CONF_PACKAGES, + CONF_REF, + CONF_REFRESH, + CONF_URL, +) import esphome.config_validation as cv -from esphome.const import CONF_PACKAGES +DOMAIN = CONF_PACKAGES def _merge_package(full_old, full_new): @@ -23,11 +36,119 @@ def _merge_package(full_old, full_new): return merge(full_old, full_new) +def validate_git_package(config: dict): + new_config = config + for key, conf in config.items(): + if CONF_URL in conf: + try: + conf = BASE_SCHEMA(conf) + if CONF_FILE in conf: + new_config[key][CONF_FILES] = [conf[CONF_FILE]] + del new_config[key][CONF_FILE] + except cv.MultipleInvalid as e: + with cv.prepend_path([key]): + raise e + except cv.Invalid as e: + raise cv.Invalid( + "Extra keys not allowed in git based package", + path=[key] + e.path, + ) from e + return new_config + + +def validate_yaml_filename(value): + value = cv.string(value) + + if not (value.endswith(".yaml") or value.endswith(".yml")): + raise cv.Invalid("Only YAML (.yaml / .yml) files are supported.") + + return value + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + + m = re.match( + r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)/([a-zA-Z0-9\-_.\./]+)(?:@([a-zA-Z0-9\-_.\./]+))?", + value, + ) + if m is None: + raise cv.Invalid( + "Source is not a file system path or in expected github://username/name/[sub-folder/]file-path.yml[@branch-or-tag] format!" + ) + + conf = { + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + CONF_FILE: m.group(3), + } + if m.group(4): + conf[CONF_REF] = m.group(4) + + # print(conf) + return BASE_SCHEMA(conf) + + +BASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_URL): cv.url, + cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename, + cv.Exclusive(CONF_FILES, "files"): cv.All( + cv.ensure_list(validate_yaml_filename), + cv.Length(min=1), + ), + cv.Optional(CONF_REF): cv.git_ref, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ), + cv.has_at_least_one_key(CONF_FILE, CONF_FILES), +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + str: cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), + } + ), + validate_git_package, +) + + +def _process_base_package(config: dict) -> dict: + repo_dir = git.clone_or_update( + url=config[CONF_URL], + ref=config.get(CONF_REF), + refresh=config[CONF_REFRESH], + domain=DOMAIN, + ) + files: str = config[CONF_FILES] + + packages = {} + for file in files: + yaml_file: Path = repo_dir / file + + if not yaml_file.is_file(): + raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES]) + + try: + packages[file] = yaml_util.load_yaml(yaml_file) + except EsphomeError as e: + raise cv.Invalid( + f"{file} is not a valid YAML file. Please check the file contents." + ) from e + return {"packages": packages} + + def do_packages_pass(config: dict): if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] with cv.prepend_path(CONF_PACKAGES): + packages = CONFIG_SCHEMA(packages) if not isinstance(packages, dict): raise cv.Invalid( "Packages must be a key to value mapping, got {} instead" @@ -37,6 +158,8 @@ def do_packages_pass(config: dict): for package_name, package_config in packages.items(): with cv.prepend_path(package_name): recursive_package = package_config + if CONF_URL in package_config: + package_config = _process_base_package(package_config) if isinstance(package_config, dict): recursive_package = do_packages_pass(package_config) config = _merge_package(recursive_package, config) diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h index 687fe562d1..f74001cf75 100644 --- a/esphome/components/partition/light_partition.h +++ b/esphome/components/partition/light_partition.h @@ -50,13 +50,11 @@ class PartitionLightOutput : public light::AddressableLight { } } light::LightTraits get_traits() override { return this->segments_[0].get_src()->get_traits(); } - void loop() override { - if (this->should_show_()) { - for (auto seg : this->segments_) { - seg.get_src()->schedule_show(); - } - this->mark_shown_(); + void write_state(light::LightState *state) override { + for (auto seg : this->segments_) { + seg.get_src()->schedule_show(); } + this->mark_shown_(); } protected: diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index 9bedb3cfc0..1a89307eee 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -7,6 +7,7 @@ namespace pm1006 { static const char *const TAG = "pm1006"; static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B}; +static const uint8_t PM1006_REQUEST[] = {0x11, 0x02, 0x0B, 0x01, 0xE1}; void PM1006Component::setup() { // because this implementation is currently rx-only, there is nothing to setup @@ -15,9 +16,15 @@ void PM1006Component::setup() { void PM1006Component::dump_config() { ESP_LOGCONFIG(TAG, "PM1006:"); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_UPDATE_INTERVAL(this); this->check_uart_settings(9600); } +void PM1006Component::update() { + ESP_LOGV(TAG, "sending measurement request"); + this->write_array(PM1006_REQUEST, sizeof(PM1006_REQUEST)); +} + void PM1006Component::loop() { while (this->available() != 0) { this->read_byte(&this->data_[this->data_index_]); diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h index 66f4cf0311..238ac67006 100644 --- a/esphome/components/pm1006/pm1006.h +++ b/esphome/components/pm1006/pm1006.h @@ -7,7 +7,7 @@ namespace esphome { namespace pm1006 { -class PM1006Component : public Component, public uart::UARTDevice { +class PM1006Component : public PollingComponent, public uart::UARTDevice { public: PM1006Component() = default; @@ -15,6 +15,7 @@ class PM1006Component : public Component, public uart::UARTDevice { void setup() override; void dump_config() override; void loop() override; + void update() override; float get_setup_priority() const override; diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index 8ea0e303f3..1e648be199 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -4,11 +4,15 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_ID, CONF_PM_2_5, + CONF_UPDATE_INTERVAL, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_BLUR, ) +from esphome.core import TimePeriodMilliseconds +CODEOWNERS = ["@habbie"] DEPENDENCIES = ["uart"] pm1006_ns = cg.esphome_ns.namespace("pm1006") @@ -23,15 +27,34 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_BLUR, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), } ) .extend(cv.COMPONENT_SCHEMA) - .extend(uart.UART_DEVICE_SCHEMA), + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.polling_component_schema("never")), ) +def validate_interval_uart(config): + require_tx = False + + interval = config.get(CONF_UPDATE_INTERVAL) + + if isinstance(interval, TimePeriodMilliseconds): + # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects + require_tx = True + + uart.final_validate_device_schema( + "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx + )(config) + + +FINAL_VALIDATE_SCHEMA = validate_interval_uart + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py index ac26270cfc..ceca791cd6 100644 --- a/esphome/components/pmsa003i/sensor.py +++ b/esphome/components/pmsa003i/sensor.py @@ -13,7 +13,10 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, ICON_COUNTER, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, ) CODEOWNERS = ["@sjtrny"] @@ -36,40 +39,61 @@ CONFIG_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, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_COUNTS_PER_100ML, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) @@ -90,15 +114,15 @@ TYPES = { } -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + await 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]) + sens = await sensor.new_sensor(config[key]) cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index c3dd7d5a97..8b9e5c9af2 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -19,6 +19,9 @@ from esphome.const import ( CONF_PM_10_0UM, CONF_TEMPERATURE, CONF_TYPE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -75,19 +78,19 @@ CONFIG_SCHEMA = ( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM1, ), cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM25, ), cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, - DEVICE_CLASS_EMPTY, + DEVICE_CLASS_PM10, ), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index f50adac6f9..a492919202 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -42,7 +42,7 @@ void PowerSupply::request_high_power() { void PowerSupply::unrequest_high_power() { this->active_requests_--; if (this->active_requests_ < 0) { - // we're just going to use 0 as our now counter. + // we're just going to use 0 as our new counter. this->active_requests_ = 0; } diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 767728fc80..c7b89d41b0 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TOTAL, ICON_PULSE, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_PULSES_PER_MINUTE, UNIT_PULSES, ) @@ -95,7 +95,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PULSES, icon=ICON_PULSE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 18da842bad..454cb3a69d 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -11,8 +11,8 @@ from esphome.const import ( CONF_TOTAL, CONF_VALUE, ICON_PULSE, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_PULSES, UNIT_PULSES_PER_MINUTE, ) @@ -64,8 +64,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_PULSES, icon=ICON_PULSE, accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index 23502e849a..70dec82c3f 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -11,8 +11,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -50,8 +50,7 @@ CONFIG_SCHEMA = ( 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 1616bf0ace..b6697e3d19 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -15,8 +15,8 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, @@ -55,8 +55,7 @@ CONFIG_SCHEMA = ( 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index c9f1c611a8..d76dc6bc34 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1085,3 +1085,45 @@ async def panasonic_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32) cg.add(var.set_command(template_)) + + +# Midea +MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( + "Midea" +) +MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) +MIDEA_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.hex_uint8_t, cv.uint8_t)], + cv.Length(min=5, max=5), + ), + cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.uint8), + } +) + + +@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA) +def midea_binary_sensor(var, config): + arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE]) + cg.add(var.set_code(arr_)) + + +@register_trigger("midea", MideaTrigger, MideaData) +def midea_trigger(var, config): + pass + + +@register_dumper("midea", MideaDumper) +def midea_dumper(var, config): + pass + + +@register_action( + "midea", + MideaAction, + MIDEA_SCHEMA, +) +async def midea_action(var, config, args): + arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE]) + cg.add(var.set_code(arr_)) diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp new file mode 100644 index 0000000000..baf64f246f --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -0,0 +1,99 @@ +#include "midea_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.midea"; + +uint8_t MideaData::calc_cs_() const { + uint8_t cs = 0; + for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it) + cs -= reverse_bits_8(*it); + return reverse_bits_8(cs); +} + +bool MideaData::check_compliment(const MideaData &rhs) const { + const uint8_t *it0 = rhs.data(); + for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) { + if (*it0 != ~(*it1)) + return false; + } + return true; +} + +void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) { + for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) { + const uint8_t data = compliment ? ~(*it) : *it; + for (uint8_t mask = 128; mask; mask >>= 1) { + if (data & mask) + one(dst); + else + zero(dst); + } + } +} + +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data); + MideaProtocol::footer(dst); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data, true); + MideaProtocol::footer(dst); +} + +bool MideaProtocol::expect_one(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_zero(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_header(RemoteReceiveData &src) { + if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_footer(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) { + for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) { + for (uint8_t mask = 128; mask; mask >>= 1) { + if (MideaProtocol::expect_one(src)) + *dst |= mask; + else if (!MideaProtocol::expect_zero(src)) + return false; + } + } + return true; +} + +optional MideaProtocol::decode(RemoteReceiveData src) { + MideaData out, inv; + if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) && + out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv)) + return out; + return {}; +} + +void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(TAG, "Received Midea: %s", data.to_string().c_str()); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h new file mode 100644 index 0000000000..9b0d156617 --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +class MideaData { + public: + // Make zero-filled + MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make from initializer_list + MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + // Make from vector + MideaData(const std::vector &data) { + memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + } + // Make 40-bit copy from PROGMEM array + MideaData(const uint8_t *data) { memcpy_P(this->data_, data, OFFSET_CS); } + // Default copy constructor + MideaData(const MideaData &) = default; + + uint8_t *data() { return this->data_; } + const uint8_t *data() const { return this->data_; } + uint8_t size() const { return sizeof(this->data_); } + bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } + void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } + bool check_compliment(const MideaData &rhs) const; + std::string to_string() const { return hexencode(*this); } + // compare only 40-bits + bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + enum MideaDataType : uint8_t { + MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_SPECIAL = 0xA2, + MIDEA_TYPE_FOLLOW_ME = 0xA4, + }; + MideaDataType type() const { return static_cast(this->data_[0]); } + template T to() const { return T(*this); } + + protected: + void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) { + data_[offset] &= ~(val_mask << shift); + data_[offset] |= (val << shift); + } + static const uint8_t OFFSET_CS = 5; + // 48-bits data + uint8_t data_[6]; + // Calculate checksum + uint8_t calc_cs_() const; +}; + +class MideaProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MideaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MideaData &data) override; + + protected: + static const int32_t TICK_US = 560; + static const int32_t HEADER_HIGH_US = 8 * TICK_US; + static const int32_t HEADER_LOW_US = 8 * TICK_US; + static const int32_t BIT_HIGH_US = 1 * TICK_US; + static const int32_t BIT_ONE_LOW_US = 3 * TICK_US; + static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US; + static const int32_t MIN_GAP_US = 10 * TICK_US; + static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); } + static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } + static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); } + static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); } + static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false); + static bool expect_one(RemoteReceiveData &src); + static bool expect_zero(RemoteReceiveData &src); + static bool expect_header(RemoteReceiveData &src); + static bool expect_footer(RemoteReceiveData &src); + static bool expect_data(RemoteReceiveData &src, MideaData &out); +}; + +class MideaBinarySensor : public RemoteReceiverBinarySensorBase { + public: + bool matches(RemoteReceiveData src) override { + auto data = MideaProtocol().decode(src); + return data.has_value() && data.value() == this->data_; + } + void set_code(const uint8_t *code) { this->data_ = code; } + + protected: + MideaData data_; +}; + +using MideaTrigger = RemoteReceiverTrigger; +using MideaDumper = RemoteReceiverDumper; + +template class MideaAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(const uint8_t *, code) + void encode(RemoteTransmitData *dst, Ts... x) override { + MideaData data = this->code_.value(x...); + data.finalize(); + MideaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 13cc94786c..8a0d9674a7 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -23,17 +23,17 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_FLASH, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_DEGREES, UNIT_HERTZ, + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_VOLT_AMPS, UNIT_VOLT_AMPS_REACTIVE, - UNIT_VOLT_AMPS_REACTIVE_HOURS, UNIT_WATT, - UNIT_WATT_HOURS, ) AUTO_LOAD = ["modbus"] @@ -47,6 +47,7 @@ PHASE_SENSORS = { 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, @@ -100,32 +101,28 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, + 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, + 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ) diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index 0997b47ef6..456d47ee91 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -7,6 +7,8 @@ from esphome.const import ( CONF_PM_2_5, CONF_RX_ONLY, CONF_UPDATE_INTERVAL, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -41,12 +43,14 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=1, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=1, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_RX_ONLY, default=False): cv.boolean, diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 2d05d00380..168d3a3db2 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -20,8 +20,8 @@ from esphome.const import ( DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_HERTZ, UNIT_VOLT, @@ -54,50 +54,43 @@ SENSORS = { 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), 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, + state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 739a8ada50..d423793873 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_CO2, CONF_ID, ICON_MOLECULE_CO2, + DEVICE_CLASS_CARBON_DIOXIDE, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) @@ -41,6 +42,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 1bb4e25a17..fd278be51e 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -17,7 +17,6 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INTERNAL, - CONF_LAST_RESET_TYPE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -31,25 +30,32 @@ from esphome.const import ( CONF_NAME, CONF_MQTT_ID, CONF_FORCE_UPDATE, - LAST_RESET_TYPE_AUTO, - LAST_RESET_TYPE_NEVER, - LAST_RESET_TYPE_NONE, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, ) from esphome.core import CORE, coroutine_with_priority @@ -58,21 +64,31 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, + DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MONETARY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, ] @@ -85,15 +101,6 @@ STATE_CLASSES = { } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") -LastResetTypes = sensor_ns.enum("LastResetType") -LAST_RESET_TYPES = { - LAST_RESET_TYPE_NONE: LastResetTypes.LAST_RESET_TYPE_NONE, - LAST_RESET_TYPE_NEVER: LastResetTypes.LAST_RESET_TYPE_NEVER, - LAST_RESET_TYPE_AUTO: LastResetTypes.LAST_RESET_TYPE_AUTO, -} -validate_last_reset_type = cv.enum(LAST_RESET_TYPES, lower=True, space="_") - - IS_PLATFORM_COMPONENT = True @@ -183,7 +190,9 @@ SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( 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("last_reset_type"): cv.invalid( + "last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values." + ), cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All( cv.requires_component("mqtt"), @@ -220,7 +229,6 @@ def sensor_schema( 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 is not _UNDEF: @@ -253,14 +261,6 @@ def sensor_schema( schema = schema.extend( {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 - } - ) return schema @@ -511,8 +511,6 @@ async def setup_sensor_core_(var, config): cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) - if CONF_LAST_RESET_TYPE in config: - cg.add(var.set_last_reset_type(config[CONF_LAST_RESET_TYPE])) cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 1a5c76db51..1dbc1c901a 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -18,18 +18,6 @@ const char *state_class_to_string(StateClass state_class) { } } -const char *last_reset_type_to_string(LastResetType last_reset_type) { - switch (last_reset_type) { - case LAST_RESET_TYPE_NEVER: - return "never"; - case LAST_RESET_TYPE_AUTO: - return "auto"; - case LAST_RESET_TYPE_NONE: - default: - return ""; - } -} - void Sensor::publish_state(float state) { this->raw_state = state; this->raw_callback_.call(state); @@ -80,7 +68,6 @@ void Sensor::set_state_class(const std::string &state_class) { ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str()); } } -void Sensor::set_last_reset_type(LastResetType last_reset_type) { this->last_reset_type = last_reset_type; } std::string Sensor::get_unit_of_measurement() { if (this->unit_of_measurement_.has_value()) return *this->unit_of_measurement_; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f0d7ba4887..34b8b26a54 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -14,10 +14,6 @@ namespace sensor { ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ ESP_LOGCONFIG(TAG, "%s State Class: '%s'", prefix, state_class_to_string((obj)->state_class)); \ - if ((obj)->state_class == sensor::STATE_CLASS_MEASUREMENT && \ - (obj)->last_reset_type != sensor::LAST_RESET_TYPE_NONE) { \ - ESP_LOGCONFIG(TAG, "%s Last Reset Type: '%s'", prefix, last_reset_type_to_string((obj)->last_reset_type)); \ - } \ ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->get_unit_of_measurement().c_str()); \ ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, (obj)->get_accuracy_decimals()); \ if (!(obj)->get_icon().empty()) { \ @@ -42,20 +38,6 @@ enum StateClass : uint8_t { const char *state_class_to_string(StateClass state_class); -/** - * Sensor last reset types - */ -enum LastResetType : uint8_t { - /// This sensor does not support resetting. ie, it is not accumulative - LAST_RESET_TYPE_NONE = 0, - /// This sensor is expected to never reset its value - LAST_RESET_TYPE_NEVER = 1, - /// This sensor may reset and Home Assistant will watch for this - LAST_RESET_TYPE_AUTO = 2, -}; - -const char *last_reset_type_to_string(LastResetType last_reset_type); - /** Base-class for all sensors. * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. @@ -174,12 +156,6 @@ class Sensor : public Nameable { */ virtual std::string device_class(); - // The Last reset type of this sensor - LastResetType last_reset_type{LAST_RESET_TYPE_NONE}; - - /// Manually set the Home Assistant last reset type for this sensor. - void set_last_reset_type(LastResetType last_reset_type); - /** A unique ID for this sensor, empty for no unique id. See unique ID requirements: * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements * diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 3e33af3b4a..2596e0065d 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -7,6 +7,8 @@ from esphome.const import ( CONF_ECO2, CONF_TVOC, ICON_RADIATOR, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -34,12 +36,14 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 0f562048ac..7b96f867af 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -4,6 +4,7 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, ICON_RADIATOR, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, ) @@ -26,6 +27,7 @@ CONFIG_SCHEMA = ( sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ) .extend( diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 8d93b3e1b1..a911c107b9 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -78,27 +78,28 @@ void SGP40Component::setup() { } void SGP40Component::self_test_() { - ESP_LOGD(TAG, "selfTest started"); + ESP_LOGD(TAG, "Self-test started"); if (!this->write_command_(SGP40_CMD_SELF_TEST)) { this->error_code_ = COMMUNICATION_FAILED; - ESP_LOGD(TAG, "selfTest communicatin failed"); + ESP_LOGD(TAG, "Self-test communication failed"); this->mark_failed(); } this->set_timeout(250, [this]() { uint16_t reply[1]; if (!this->read_data_(reply, 1)) { - ESP_LOGD(TAG, "selfTest read_data_ failed"); + ESP_LOGD(TAG, "Self-test read_data_ failed"); this->mark_failed(); return; } if (reply[0] == 0xD400) { - ESP_LOGD(TAG, "selfTest completed"); + this->self_test_complete_ = true; + ESP_LOGD(TAG, "Self-test completed"); return; } - ESP_LOGD(TAG, "selfTest failed"); + ESP_LOGD(TAG, "Self-test failed"); this->mark_failed(); }); } @@ -154,6 +155,12 @@ int32_t SGP40Component::measure_voc_index_() { */ uint16_t SGP40Component::measure_raw_() { float humidity = NAN; + + if (!this->self_test_complete_) { + ESP_LOGD(TAG, "Self-test not yet complete"); + return UINT16_MAX; + } + if (this->humidity_sensor_ != nullptr) { humidity = this->humidity_sensor_->state; } diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h index b9ea365169..62936102e7 100644 --- a/esphome/components/sgp40/sgp40.h +++ b/esphome/components/sgp40/sgp40.h @@ -68,6 +68,7 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2 int32_t seconds_since_last_store_; SGP40Baselines baselines_storage_; VocAlgorithmParams voc_algorithm_params_; + bool self_test_complete_; bool store_baseline_; int32_t state0_; int32_t state1_; diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py index 73cada0eb3..8452ee81f2 100644 --- a/esphome/components/sm300d2/sensor.py +++ b/esphome/components/sm300d2/sensor.py @@ -10,6 +10,10 @@ from esphome.const import ( CONF_PM_10_0, CONF_TEMPERATURE, CONF_HUMIDITY, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT, @@ -36,6 +40,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( @@ -48,18 +53,21 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_GRAIN, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_GRAIN, accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index ff176b1d4e..895c775b19 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -56,9 +56,8 @@ void SNTPComponent::loop() { if (!time.is_valid()) return; - char buf[128]; - time.strftime(buf, sizeof(buf), "%c"); - ESP_LOGD(TAG, "Synchronized time: %s", buf); + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); this->time_sync_callback_.call(); this->has_time_ = true; } diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py new file mode 100644 index 0000000000..8e9502be6d --- /dev/null +++ b/esphome/components/socket/__init__.py @@ -0,0 +1,28 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +CODEOWNERS = ["@esphome/core"] + +CONF_IMPLEMENTATION = "implementation" +IMPLEMENTATION_LWIP_TCP = "lwip_tcp" +IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" + +CONFIG_SCHEMA = cv.Schema( + { + cv.SplitDefault( + CONF_IMPLEMENTATION, + esp8266=IMPLEMENTATION_LWIP_TCP, + esp32=IMPLEMENTATION_BSD_SOCKETS, + ): cv.one_of( + IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" + ), + } +) + + +async def to_code(config): + impl = config[CONF_IMPLEMENTATION] + if impl == IMPLEMENTATION_LWIP_TCP: + cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") + elif impl == IMPLEMENTATION_BSD_SOCKETS: + cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp new file mode 100644 index 0000000000..a0cdb6ec42 --- /dev/null +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -0,0 +1,105 @@ +#include "socket.h" +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include + +namespace esphome { +namespace socket { + +std::string format_sockaddr(const struct sockaddr_storage &storage) { + if (storage.ss_family == AF_INET) { + const struct sockaddr_in *addr = reinterpret_cast(&storage); + char buf[INET_ADDRSTRLEN]; + const char *ret = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); + if (ret == NULL) + return {}; + return std::string{buf}; + } else if (storage.ss_family == AF_INET6) { + const struct sockaddr_in6 *addr = reinterpret_cast(&storage); + char buf[INET6_ADDRSTRLEN]; + const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); + if (ret == NULL) + return {}; + return std::string{buf}; + } + return {}; +} + +class BSDSocketImpl : public Socket { + public: + BSDSocketImpl(int fd) : Socket(), fd_(fd) {} + ~BSDSocketImpl() override { + if (!closed_) { + close(); + } + } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = ::accept(fd_, addr, addrlen); + if (fd == -1) + return {}; + return std::unique_ptr{new BSDSocketImpl(fd)}; + } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); } + int close() override { + int ret = ::close(fd_); + closed_ = true; + return ret; + } + int shutdown(int how) override { return ::shutdown(fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); } + std::string getpeername() override { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + int err = this->getpeername((struct sockaddr *) &storage, &len); + if (err != 0) + return {}; + return format_sockaddr(storage); + } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); } + std::string getsockname() override { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + int err = this->getsockname((struct sockaddr *) &storage, &len); + if (err != 0) + return {}; + return format_sockaddr(storage); + } + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + return ::getsockopt(fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + return ::setsockopt(fd_, level, optname, optval, optlen); + } + int listen(int backlog) override { return ::listen(fd_, backlog); } + ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } + ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); } + int setblocking(bool blocking) override { + int fl = ::fcntl(fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + ::fcntl(fd_, F_SETFL, fl); + return 0; + } + + protected: + int fd_; + bool closed_ = false; +}; + +std::unique_ptr socket(int domain, int type, int protocol) { + int ret = ::socket(domain, type, protocol); + if (ret == -1) + return nullptr; + return std::unique_ptr{new BSDSocketImpl(ret)}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h new file mode 100644 index 0000000000..da710b760e --- /dev/null +++ b/esphome/components/socket/headers.h @@ -0,0 +1,127 @@ +#pragma once +#include "esphome/core/defines.h" + +// Helper file to include all socket-related system headers (or use our own +// definitions where system ones don't exist) + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#define LWIP_INTERNAL +#include +#include "lwip/inet.h" +#include +#include + +/* Address families. */ +#define AF_UNSPEC 0 +#define AF_INET 2 +#define AF_INET6 10 +#define PF_INET AF_INET +#define PF_INET6 AF_INET6 +#define PF_UNSPEC AF_UNSPEC +#define IPPROTO_IP 0 +#define IPPROTO_TCP 6 +#define IPPROTO_IPV6 41 +#define IPPROTO_ICMPV6 58 + +#define TCP_NODELAY 0x01 + +#define F_GETFL 3 +#define F_SETFL 4 +#define O_NONBLOCK 1 + +#define SHUT_RD 0 +#define SHUT_WR 1 +#define SHUT_RDWR 2 + +/* Socket protocol types (TCP/UDP/RAW) */ +#define SOCK_STREAM 1 +#define SOCK_DGRAM 2 +#define SOCK_RAW 3 + +#define SO_REUSEADDR 0x0004 /* Allow local address reuse */ +#define SO_KEEPALIVE 0x0008 /* keep connections alive */ +#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ + +#define SOL_SOCKET 0xfff /* options for socket level */ + +typedef uint8_t sa_family_t; +typedef uint16_t in_port_t; + +struct sockaddr_in { + uint8_t sin_len; + sa_family_t sin_family; + in_port_t sin_port; + struct in_addr sin_addr; +#define SIN_ZERO_LEN 8 + char sin_zero[SIN_ZERO_LEN]; +}; + +struct sockaddr_in6 { + uint8_t sin6_len; /* length of this structure */ + sa_family_t sin6_family; /* AF_INET6 */ + in_port_t sin6_port; /* Transport layer port # */ + uint32_t sin6_flowinfo; /* IPv6 flow information */ + struct in6_addr sin6_addr; /* IPv6 address */ + uint32_t sin6_scope_id; /* Set of interfaces for scope */ +}; + +struct sockaddr { + uint8_t sa_len; + sa_family_t sa_family; + char sa_data[14]; +}; + +struct sockaddr_storage { + uint8_t s2_len; + sa_family_t ss_family; + char s2_data1[2]; + uint32_t s2_data2[3]; + uint32_t s2_data3[3]; +}; +typedef uint32_t socklen_t; + +#ifdef ARDUINO_ARCH_ESP8266 +// arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define +#ifdef INADDR_ANY +#undef INADDR_ANY +#endif +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif + +#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL) +#else // !ARDUINO_ARCH_ESP8266 +#define ESPHOME_INADDR_ANY INADDR_ANY +#define ESPHOME_INADDR_NONE INADDR_NONE +#endif + +#endif // USE_SOCKET_IMPL_LWIP_TCP + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include +#include +#include +#include +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 +// arduino-esp32 declares a global var called INADDR_NONE which is replaced +// by the define +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif +// not defined for ESP32 +typedef uint32_t socklen_t; + +#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL) +#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL) +#else // !ARDUINO_ARCH_ESP32 +#define ESPHOME_INADDR_ANY INADDR_ANY +#define ESPHOME_INADDR_NONE INADDR_NONE +#endif + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp new file mode 100644 index 0000000000..aaeee7268a --- /dev/null +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -0,0 +1,497 @@ +#include "socket.h" +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#include "lwip/ip.h" +#include "lwip/netif.h" +#include "lwip/opt.h" +#include "lwip/tcp.h" +#include +#include +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace socket { + +static const char *const TAG = "socket.lwip"; + +// set to 1 to enable verbose lwip logging +#if 0 +#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) +#else +#define LWIP_LOG(msg, ...) +#endif + +class LWIPRawImpl : public Socket { + public: + LWIPRawImpl(struct tcp_pcb *pcb) : pcb_(pcb) {} + ~LWIPRawImpl() override { + if (pcb_ != nullptr) { + LWIP_LOG("tcp_abort(%p)", pcb_); + tcp_abort(pcb_); + pcb_ = nullptr; + } + } + + void init() { + LWIP_LOG("init(%p)", pcb_); + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); + tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); + tcp_err(pcb_, LWIPRawImpl::s_err_fn); + } + + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return nullptr; + } + if (accepted_sockets_.empty()) { + errno = EWOULDBLOCK; + return nullptr; + } + std::unique_ptr sock = std::move(accepted_sockets_.front()); + accepted_sockets_.pop(); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return std::unique_ptr(std::move(sock)); + } + int bind(const struct sockaddr *name, socklen_t addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr) { + errno = EINVAL; + return 0; + } + ip_addr_t ip; + in_port_t port; + auto family = name->sa_family; +#if LWIP_IPV6 + if (family == AF_INET) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.type = IPADDR_TYPE_V4; + ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; + + } else if (family == AF_INET6) { + if (addrlen < sizeof(sockaddr_in)) { + errno = EINVAL; + return -1; + } + auto *addr6 = reinterpret_cast(name); + port = ntohs(addr6->sin6_port); + ip.type = IPADDR_TYPE_V6; + memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); + } else { + errno = EINVAL; + return -1; + } +#else + if (family != AF_INET) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.addr = addr4->sin_addr.s_addr; +#endif + LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port); + err_t err = tcp_bind(pcb_, &ip, port); + if (err == ERR_USE) { + errno = EADDRINUSE; + return -1; + } + if (err == ERR_VAL) { + errno = EINVAL; + return -1; + } + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return 0; + } + int close() override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + LWIP_LOG("tcp_close(%p)", pcb_); + err_t err = tcp_close(pcb_); + if (err != ERR_OK) { + tcp_abort(pcb_); + pcb_ = nullptr; + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + pcb_ = nullptr; + return 0; + } + int shutdown(int how) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + bool shut_rx = false, shut_tx = false; + if (how == SHUT_RD) { + shut_rx = true; + } else if (how == SHUT_WR) { + shut_tx = true; + } else if (how == SHUT_RDWR) { + shut_rx = shut_tx = true; + } else { + errno = EINVAL; + return -1; + } + LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); + err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); + if (err != ERR_OK) { + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + return 0; + } + + int getpeername(struct sockaddr *name, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = pcb_->remote_port; + addr->sin_addr.s_addr = pcb_->remote_ip.addr; + return 0; + } + std::string getpeername() override { + if (pcb_ == nullptr) { + errno = EBADF; + return ""; + } + char buffer[24]; + uint32_t ip4 = pcb_->remote_ip.addr; + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF, + (ip4 >> 24) & 0xFF); + return std::string(buffer); + } + int getsockname(struct sockaddr *name, socklen_t *addrlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = pcb_->local_port; + addr->sin_addr.s_addr = pcb_->local_ip.addr; + return 0; + } + std::string getsockname() override { + if (pcb_ == nullptr) { + errno = EBADF; + return ""; + } + char buffer[24]; + uint32_t ip4 = pcb_->local_ip.addr; + snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF, + (ip4 >> 24) & 0xFF); + return std::string(buffer); + } + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (optlen == nullptr || optval == nullptr) { + errno = EINVAL; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (*optlen < 4) { + errno = EINVAL; + return -1; + } + + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + *reinterpret_cast(optval) = 1; + *optlen = 4; + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (*optlen < 4) { + errno = EINVAL; + return -1; + } + *reinterpret_cast(optval) = tcp_nagle_disabled(pcb_); + *optlen = 4; + return 0; + } + + errno = EINVAL; + return -1; + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + int val = *reinterpret_cast(optval); + if (val != 0) { + tcp_nagle_disable(pcb_); + } else { + tcp_nagle_enable(pcb_); + } + return 0; + } + + errno = EINVAL; + return -1; + } + int listen(int backlog) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); + struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); + if (listen_pcb == nullptr) { + tcp_abort(pcb_); + pcb_ = nullptr; + errno = EOPNOTSUPP; + return -1; + } + // tcp_listen reallocates the pcb, replace ours + pcb_ = listen_pcb; + // set callbacks on new pcb + LWIP_LOG("tcp_arg(%p)", pcb_); + tcp_arg(pcb_, this); + tcp_accept(pcb_, LWIPRawImpl::s_accept_fn); + return 0; + } + ssize_t read(void *buf, size_t len) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (rx_closed_ && rx_buf_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (len == 0) { + return 0; + } + if (rx_buf_ == nullptr) { + errno = EWOULDBLOCK; + return -1; + } + + size_t read = 0; + uint8_t *buf8 = reinterpret_cast(buf); + while (len && rx_buf_ != nullptr) { + size_t pb_len = rx_buf_->len; + size_t pb_left = pb_len - rx_buf_offset_; + if (pb_left == 0) + break; + size_t copysize = std::min(len, pb_left); + memcpy(buf8, reinterpret_cast(rx_buf_->payload) + rx_buf_offset_, copysize); + + if (pb_left == copysize) { + // full pb copied, free it + if (rx_buf_->next == nullptr) { + // last buffer in chain + pbuf_free(rx_buf_); + rx_buf_ = nullptr; + rx_buf_offset_ = 0; + } else { + auto *old_buf = rx_buf_; + rx_buf_ = rx_buf_->next; + pbuf_ref(rx_buf_); + pbuf_free(old_buf); + rx_buf_offset_ = 0; + } + } else { + rx_buf_offset_ += copysize; + } + LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize); + tcp_recved(pcb_, copysize); + + buf8 += copysize; + len -= copysize; + read += copysize; + } + + return read; + } + ssize_t write(const void *buf, size_t len) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (len == 0) + return 0; + if (buf == nullptr) { + errno = EINVAL; + return 0; + } + auto space = tcp_sndbuf(pcb_); + if (space == 0) { + errno = EWOULDBLOCK; + return -1; + } + size_t to_send = std::min((size_t) space, len); + LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send); + err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); + if (err == ERR_MEM) { + errno = EWOULDBLOCK; + return -1; + } + if (err != ERR_OK) { + errno = EIO; + return -1; + } + LWIP_LOG("tcp_output(%p)", pcb_); + err = tcp_output(pcb_); + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return to_send; + } + int setblocking(bool blocking) override { + if (pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (blocking) { + // blocking operation not supported + errno = EINVAL; + return -1; + } + return 0; + } + + err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); + if (err != ERR_OK || newpcb == nullptr) { + // "An error code if there has been an error accepting. Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d + // nothing to do here, we just don't push it to the queue + return ERR_OK; + } + auto *sock = new LWIPRawImpl(newpcb); + sock->init(); + accepted_sockets_.emplace(sock); + return ERR_OK; + } + void err_fn(err_t err) { + LWIP_LOG("err(err=%d)", err); + // "If a connection is aborted because of an error, the application is alerted of this event by + // the err callback." + // pcb is already freed when this callback is called + // ERR_RST: connection was reset by remote host + // ERR_ABRT: aborted through tcp_abort or TCP timer + pcb_ = nullptr; + } + err_t recv_fn(struct pbuf *pb, err_t err) { + LWIP_LOG("recv(pb=%p err=%d)", pb, err); + if (err != 0) { + // "An error code if there has been an error receiving Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + rx_closed_ = true; + return ERR_OK; + } + if (pb == nullptr) { + rx_closed_ = true; + return ERR_OK; + } + if (rx_buf_ == nullptr) { + // no need to copy because lwIP gave control of it to us + rx_buf_ = pb; + rx_buf_offset_ = 0; + } else { + pbuf_cat(rx_buf_, pb); + } + return ERR_OK; + } + + static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->accept_fn(newpcb, err); + } + + static void s_err_fn(void *arg, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->err_fn(err); + } + + static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + LWIPRawImpl *arg_this = reinterpret_cast(arg); + return arg_this->recv_fn(pb, err); + } + + protected: + struct tcp_pcb *pcb_; + std::queue> accepted_sockets_; + bool rx_closed_ = false; + pbuf *rx_buf_ = nullptr; + size_t rx_buf_offset_ = 0; +}; + +std::unique_ptr socket(int domain, int type, int protocol) { + auto *pcb = tcp_new(); + if (pcb == nullptr) + return nullptr; + auto *sock = new LWIPRawImpl(pcb); + sock->init(); + return std::unique_ptr{sock}; +} + +} // namespace socket +} // namespace esphome + +#endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h new file mode 100644 index 0000000000..7a5ce79161 --- /dev/null +++ b/esphome/components/socket/socket.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +#include "headers.h" +#include "esphome/core/optional.h" + +namespace esphome { +namespace socket { + +class Socket { + public: + Socket() = default; + virtual ~Socket() = default; + Socket(const Socket &) = delete; + Socket &operator=(const Socket &) = delete; + + virtual std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0; + virtual int close() = 0; + // not supported yet: + // virtual int connect(const std::string &address) = 0; + // virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; + virtual int shutdown(int how) = 0; + + virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual std::string getpeername() = 0; + virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; + virtual std::string getsockname() = 0; + virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; + virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; + virtual int listen(int backlog) = 0; + virtual ssize_t read(void *buf, size_t len) = 0; + virtual ssize_t write(const void *buf, size_t len) = 0; + virtual int setblocking(bool blocking) = 0; + virtual int loop() { return 0; }; +}; + +std::unique_ptr socket(int domain, int type, int protocol); + +} // namespace socket +} // namespace esphome diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 8c6ec54d4c..cb10db4ed4 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -56,7 +56,10 @@ void SpeedFan::loop() { ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); } } -float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } + +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float SpeedFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } } // namespace speed } // namespace esphome diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 959b427861..27264cf942 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -13,6 +13,9 @@ from esphome.const import ( CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, @@ -35,12 +38,14 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_4_0): sensor.sensor_schema( @@ -53,6 +58,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a053d00ea2..7b38b1d2c5 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = ( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, } ) @@ -49,8 +49,9 @@ async def to_code(config): reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) - bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) - cg.add(var.set_backlight_pin(bl)) + if CONF_BACKLIGHT_PIN in config: + bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) + cg.add(var.set_backlight_pin(bl)) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( diff --git a/esphome/components/st7920/__init__.py b/esphome/components/st7920/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py new file mode 100644 index 0000000000..9b544fa644 --- /dev/null +++ b/esphome/components/st7920/display.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_WIDTH, CONF_HEIGHT + +AUTO_LOAD = ["display"] +CODEOWNERS = ["@marsjan155"] +DEPENDENCIES = ["spi"] + +st7920_ns = cg.esphome_ns.namespace("st7920") +ST7920 = st7920_ns.class_( + "ST7920", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice +) +ST7920Ref = ST7920.operator("ref") + +CONFIG_SCHEMA = ( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ST7920), + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema()) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(ST7920Ref, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + cg.add(var.set_width(config[CONF_WIDTH])) + cg.add(var.set_height(config[CONF_HEIGHT])) + + await display.register_display(var, config) diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp new file mode 100644 index 0000000000..d985b0a426 --- /dev/null +++ b/esphome/components/st7920/st7920.cpp @@ -0,0 +1,146 @@ +#include "st7920.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7920 { + +static const char *const TAG = "st7920"; + +// ST7920 COMMANDS +static const uint8_t LCD_DATA = 0xFA; +static const uint8_t LCD_COMMAND = 0xF8; +static const uint8_t LCD_CLS = 0x01; +static const uint8_t LCD_HOME = 0x02; +static const uint8_t LCD_ADDRINC = 0x06; +static const uint8_t LCD_DISPLAYON = 0x0C; +static const uint8_t LCD_DISPLAYOFF = 0x08; +static const uint8_t LCD_CURSORON = 0x0E; +static const uint8_t LCD_CURSORBLINK = 0x0F; +static const uint8_t LCD_BASIC = 0x30; +static const uint8_t LCD_GFXMODE = 0x36; +static const uint8_t LCD_EXTEND = 0x34; +static const uint8_t LCD_TXTMODE = 0x34; +static const uint8_t LCD_STANDBY = 0x01; +static const uint8_t LCD_SCROLL = 0x03; +static const uint8_t LCD_SCROLLADDR = 0x40; +static const uint8_t LCD_ADDR = 0x80; +static const uint8_t LCD_LINE0 = 0x80; +static const uint8_t LCD_LINE1 = 0x90; +static const uint8_t LCD_LINE2 = 0x88; +static const uint8_t LCD_LINE3 = 0x98; + +void ST7920::setup() { + ESP_LOGCONFIG(TAG, "Setting up ST7920..."); + this->dump_config(); + this->spi_setup(); + this->init_internal_(this->get_buffer_length_()); + display_init_(); +} + +void ST7920::command_(uint8_t value) { + this->enable(); + this->send_(LCD_COMMAND, value); + this->disable(); +} + +void ST7920::data_(uint8_t value) { + this->enable(); + this->send_(LCD_DATA, value); + this->disable(); +} + +void ST7920::send_(uint8_t type, uint8_t value) { + this->write_byte(type); + this->write_byte(value & 0xF0); + this->write_byte(value << 4); +} + +void ST7920::goto_xy_(uint16_t x, uint16_t y) { + if (y >= 32 && y < 64) { + y -= 32; + x += 8; + } else if (y >= 64 && y < 64 + 32) { + y -= 32; + x += 0; + } else if (y >= 64 + 32 && y < 64 + 64) { + y -= 64; + x += 8; + } + this->command_(LCD_ADDR | y); // 6-bit (0..63) + this->command_(LCD_ADDR | x); // 4-bit (0..15) +} + +void HOT ST7920::write_display_data() { + uint8_t i, j, b; + for (j = 0; j < this->get_height_internal() / 2; j++) { + this->goto_xy_(0, j); + this->enable(); + for (i = 0; i < 16; i++) { // 16 bytes from line #0+ + b = this->buffer_[i + j * 16]; + this->send_(LCD_DATA, b); + } + for (i = 0; i < 16; i++) { // 16 bytes from line #32+ + b = this->buffer_[i + (j + 32) * 16]; + this->send_(LCD_DATA, b); + } + this->disable(); + App.feed_wdt(); + } +} + +void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } + +void ST7920::dump_config() { + LOG_DISPLAY("", "ST7920", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Height: %d", this->height_); + ESP_LOGCONFIG(TAG, " Width: %d", this->width_); +} + +float ST7920::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7920::update() { + this->clear(); + if (this->writer_local_.has_value()) // call lambda function if available + (*this->writer_local_)(*this); + this->write_display_data(); +} + +int ST7920::get_width_internal() { return this->width_; } + +int ST7920::get_height_internal() { return this->height_; } + +size_t ST7920::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT ST7920::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + ESP_LOGW(TAG, "Position out of area: %dx%d", x, y); + return; + } + int width = this->get_width_internal() / 8u; + if (color.is_on()) { + this->buffer_[y * width + x / 8] |= (0x80 >> (x & 7)); + } else { + this->buffer_[y * width + x / 8] &= ~(0x80 >> (x & 7)); + } +} + +void ST7920::display_init_() { + ESP_LOGD(TAG, "Initializing display..."); + this->command_(LCD_BASIC); // 8bit mode + this->command_(LCD_BASIC); // 8bit mode + this->command_(LCD_CLS); // clear screen + delay(12); // >10 ms delay + this->command_(LCD_ADDRINC); // cursor increment right no shift + this->command_(LCD_DISPLAYON); // D=1, C=0, B=0 + this->command_(LCD_EXTEND); // LCD_EXTEND); + this->command_(LCD_GFXMODE); // LCD_GFXMODE); + this->write_display_data(); +} + +} // namespace st7920 +} // namespace esphome diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h new file mode 100644 index 0000000000..d0258d922c --- /dev/null +++ b/esphome/components/st7920/st7920.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace st7920 { + +class ST7920; + +using st7920_writer_t = std::function; + +class ST7920 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; } + void set_height(uint16_t height) { this->height_ = height; } + void set_width(uint16_t width) { this->width_ = width; } + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void fill(Color color) override; + void write_display_data(); + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + void display_init_(); + void command_(uint8_t value); + void data_(uint8_t value); + void send_(uint8_t type, uint8_t value); + void goto_xy_(uint16_t x, uint16_t y); + void start_transaction_(); + void end_transaction_(); + + int16_t width_ = 128, height_ = 64; + optional writer_local_{}; +}; + +} // namespace st7920 +} // namespace esphome diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 22bbaacc15..887f6b15ad 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -29,12 +29,16 @@ def validate_min_max(config): def validate(config): if CONF_LAMBDA in config: - if CONF_OPTIMISTIC in config: + if config[CONF_OPTIMISTIC]: raise cv.Invalid("optimistic cannot be used with lambda") if CONF_INITIAL_VALUE in config: raise cv.Invalid("initial_value cannot be used with lambda") if CONF_RESTORE_VALUE in config: raise cv.Invalid("restore_value cannot be used with lambda") + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the number being set." + ) return config @@ -46,7 +50,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC): cv.boolean, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_INITIAL_VALUE): cv.float_, cv.Optional(CONF_RESTORE_VALUE): cv.boolean, @@ -75,8 +79,7 @@ async def to_code(config): cg.add(var.set_template(template_)) else: - if CONF_OPTIMISTIC in config: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_INITIAL_VALUE in config: cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) if CONF_RESTORE_VALUE in config: diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 4044a407f3..4eba77119d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -19,14 +19,26 @@ TemplateSelect = template_ns.class_( CONF_SET_ACTION = "set_action" -def validate_initial_value_in_options(config): - if CONF_INITIAL_OPTION in config: +def validate(config): + if CONF_LAMBDA in config: + if config[CONF_OPTIMISTIC]: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_OPTION in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + elif 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] + + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: + raise cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) return config @@ -38,13 +50,13 @@ CONFIG_SCHEMA = 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_OPTIMISTIC, default=False): 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, + validate, ) @@ -55,14 +67,12 @@ async def to_code(config): if CONF_LAMBDA in config: template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.optional.template(str) + config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) ) cg.add(var.set_template(template_)) else: - if CONF_OPTIMISTIC in config: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) if CONF_RESTORE_VALUE in config: diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 782c0ee6f9..8695880856 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -11,7 +11,7 @@ void TemplateSelect::setup() { return; std::string value; - ESP_LOGD(TAG, "Setting up Template Number"); + ESP_LOGD(TAG, "Setting up Template Select"); if (!this->restore_value_) { value = this->initial_option_; ESP_LOGD(TAG, "State from initial: %s", value.c_str()); diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 9324cb5dea..63cbd70db0 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -7,12 +7,13 @@ namespace template_ { static const char *const TAG = "template.sensor"; void TemplateSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (!isnan(this->get_raw_state())) { + this->publish_state(this->get_raw_state()); } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index b00710dfb7..6095a7c561 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -16,17 +16,38 @@ from .. import template_ns TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component) -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateSwitch), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, - cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) + +def validate(config): + if ( + not config[CONF_OPTIMISTIC] + and CONF_TURN_ON_ACTION not in config + and CONF_TURN_OFF_ACTION not in config + ): + raise cv.Invalid( + "Either optimistic mode must be enabled, or turn_on_action or turn_off_action must be set, " + "to handle the switch being set." + ) + return config + + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSwitch), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + validate, +) async def to_code(config): diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 885ad47bbf..83bebb5bcf 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -7,12 +7,13 @@ namespace template_ { static const char *const TAG = "template.text_sensor"; void TemplateTextSensor::update() { - if (!this->f_.has_value()) - return; - - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); + if (this->f_.has_value()) { + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } + } else if (this->has_state()) { + this->publish_state(this->state); } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index c2a93b5191..27a2d84da6 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -35,9 +35,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { } auto time = this->now(); - char buf[128]; - time.strftime(buf, sizeof(buf), "%c"); - ESP_LOGD(TAG, "Synchronized time: %s", buf); + ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour, + time.minute, time.second); this->time_sync_callback_.call(); } diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 6e75feae48..46eaac98eb 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -5,8 +5,8 @@ from esphome.const import ( CONF_ID, CONF_TIME_ID, DEVICE_CLASS_ENERGY, - LAST_RESET_TYPE_AUTO, - STATE_CLASS_MEASUREMENT, + CONF_METHOD, + STATE_CLASS_TOTAL_INCREASING, ) DEPENDENCIES = ["time"] @@ -14,6 +14,12 @@ DEPENDENCIES = ["time"] CONF_POWER_ID = "power_id" CONF_MIN_SAVE_INTERVAL = "min_save_interval" total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy") +TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod") +TOTAL_DAILY_ENERGY_METHODS = { + "trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID, + "left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT, + "right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT, +} TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component ) @@ -21,8 +27,7 @@ TotalDailyEnergy = total_daily_energy_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset_type=LAST_RESET_TYPE_AUTO, + state_class=STATE_CLASS_TOTAL_INCREASING, ) .extend( { @@ -32,6 +37,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_MIN_SAVE_INTERVAL, default="0s" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_METHOD, default="right"): cv.enum( + TOTAL_DAILY_ENERGY_METHODS, lower=True + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -49,3 +57,4 @@ async def to_code(config): time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time(time_)) cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL])) + cg.add(var.set_method(config[CONF_METHOD])) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 1e60442ae7..83333acab7 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -20,7 +20,9 @@ void TotalDailyEnergy::setup() { this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); } + void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } + void TotalDailyEnergy::loop() { auto t = this->time_->now(); if (!t.is_valid()) @@ -37,6 +39,7 @@ void TotalDailyEnergy::loop() { this->publish_state_and_save(0); } } + void TotalDailyEnergy::publish_state_and_save(float state) { this->total_energy_ = state; this->publish_state(state); @@ -47,13 +50,29 @@ void TotalDailyEnergy::publish_state_and_save(float state) { this->last_save_ = now; this->pref_.save(&state); } + void TotalDailyEnergy::process_new_state_(float state) { if (isnan(state)) return; const uint32_t now = millis(); + const float old_state = this->last_power_state_; + const float new_state = state; float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_energy = 0.0f; + switch (this->method_) { + case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: + delta_energy = delta_hours * (old_state + new_state) / 2.0; + break; + case TOTAL_DAILY_ENERGY_METHOD_LEFT: + delta_energy = delta_hours * old_state; + break; + case TOTAL_DAILY_ENERGY_METHOD_RIGHT: + delta_energy = delta_hours * new_state; + break; + } + this->last_power_state_ = new_state; this->last_update_ = now; - this->publish_state_and_save(this->total_energy_ + state * delta_hours); + this->publish_state_and_save(this->total_energy_ + delta_energy); } } // namespace total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 123446c534..fd71b8decc 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -8,11 +8,18 @@ namespace esphome { namespace total_daily_energy { +enum TotalDailyEnergyMethod { + TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, + TOTAL_DAILY_ENERGY_METHOD_LEFT, + TOTAL_DAILY_ENERGY_METHOD_RIGHT, +}; + 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 set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -29,11 +36,13 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { ESPPreferenceObject pref_; time::RealTimeClock *time_; Sensor *parent_; + TotalDailyEnergyMethod method_; 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}; + float last_power_state_{0.0f}; }; } // namespace total_daily_energy diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 8738b7f4a0..f060b18eba 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -80,9 +80,13 @@ void TuyaFan::write_state() { } if (this->speed_id_.has_value()) { ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed); - this->parent_->set_integer_datapoint_value(*this->speed_id_, this->fan_->speed - 1); + this->parent_->set_enum_datapoint_value(*this->speed_id_, this->fan_->speed - 1); } } +// We need a higher priority than the FanState component to make sure that the traits are set +// when that component sets itself up. +float TuyaFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } + } // namespace tuya } // namespace esphome diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index a24e7a218e..e96770d8c3 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -11,6 +11,7 @@ class TuyaFan : public Component { public: TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {} void setup() override; + float get_setup_priority() const override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 1dc1e18412..8ac00658f4 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -118,6 +118,11 @@ class UARTComponent : public Component, public Stream { uint8_t stop_bits_; uint8_t data_bits_; UARTParityOptions parity_; + + private: +#ifdef ARDUINO_ARCH_ESP8266 + static bool serial0InUse; +#endif }; #ifdef ARDUINO_ARCH_ESP32 diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index c45f48644c..5cb625f2ff 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -4,11 +4,17 @@ #include "esphome/core/helpers.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -# + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + namespace esphome { namespace uart { static const char *const TAG = "uart_esp8266"; +bool UARTComponent::serial0InUse = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + uint32_t UARTComponent::get_config() { uint32_t config = 0; @@ -49,15 +55,31 @@ void UARTComponent::setup() { // is 1 we still want to use Serial. SerialConfig config = static_cast(get_config()); - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { + if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(1) == 1 && + this->rx_pin_.value_or(3) == 3 +#ifdef USE_LOGGER + // we will use UART0 if logger isn't using it in swapped mode + && (logger::global_logger->get_hw_serial() == nullptr || + logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP) +#endif + ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); - } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { + UARTComponent::serial0InUse = true; + } else if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(15) == 15 && + this->rx_pin_.value_or(13) == 13 +#ifdef USE_LOGGER + // we will use UART0 swapped if logger isn't using it in regular mode + && (logger::global_logger->get_hw_serial() == nullptr || + logger::global_logger->get_uart() != logger::UART_SELECTION_UART0) +#endif + ) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); + UARTComponent::serial0InUse = true; } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 6ea3cca189..7989f3befc 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - STATE_CLASS_NONE, + STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, ICON_TIMER, ) @@ -16,7 +16,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_SECOND, icon=ICON_TIMER, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, + state_class=STATE_CLASS_TOTAL_INCREASING, ) .extend( { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a181f83c64..7f17767657 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_USERNAME, CONF_PASSWORD, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority AUTO_LOAD = ["json", "web_server_base"] @@ -61,9 +61,11 @@ async def to_code(config): cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: cg.add_define("WEBSERVER_CSS_INCLUDE") - with open(config[CONF_CSS_INCLUDE], "r") as myfile: + path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) + with open(file=path, mode="r", encoding="utf-8") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: cg.add_define("WEBSERVER_JS_INCLUDE") - with open(config[CONF_JS_INCLUDE], "r") as myfile: + path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) + with open(file=path, mode="r", encoding="utf-8") as myfile: cg.add(var.set_js_include(myfile.read())) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9dad61bb5b..56c75a1c58 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -398,13 +398,13 @@ std::string WebServer::fan_json(fan::FanState *obj) { if (traits.supports_speed()) { root["speed_level"] = obj->speed; switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { - case fan::FAN_SPEED_LOW: + case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "low"; break; - case fan::FAN_SPEED_MEDIUM: + case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "medium"; break; - case fan::FAN_SPEED_HIGH: + case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations) root["speed"] = "high"; break; } @@ -430,7 +430,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto call = obj->turn_on(); if (request->hasParam("speed")) { String speed = request->getParam("speed")->value(); - call.set_speed(speed.c_str()); + call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } if (request->hasParam("speed_level")) { String speed_level = request->getParam("speed_level")->value(); diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d066570cc8..c2943d0645 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -305,7 +305,7 @@ def wifi_network(config, static_ip): cg.add(ap.set_password(config[CONF_PASSWORD])) if CONF_EAP in config: cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) - cg.add_define("ESPHOME_WIFI_WPA2_EAP") + cg.add_define("USE_WIFI_WPA2_EAP") if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e99cd0e1b1..50feeb6cad 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -258,7 +258,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGV(TAG, " BSSID: Not Set"); } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); EAPAuth eap_config = ap.get_eap().value(); @@ -274,7 +274,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { #endif ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP } #endif if (ap.get_channel().has_value()) { @@ -478,7 +478,7 @@ void WiFiComponent::check_scanning_finished() { // copy manual IP (if set) connect_params.set_manual_ip(config.get_manual_ip()); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP // copy EAP parameters (if set) connect_params.set_eap(config.get_eap()); #endif @@ -638,8 +638,8 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } -#ifdef ESPHOME_WIFI_WPA2_EAP -void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } +#ifdef USE_WIFI_WPA2_EAP +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = std::move(manual_ip); } @@ -647,7 +647,7 @@ void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } @@ -679,7 +679,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) { if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) return false; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP // BSSID requires auth but no PSK or EAP credentials given if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) return false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f698e09d93..3a4213c93c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/macros.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/automation.h" @@ -17,7 +18,7 @@ #include #include -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) extern "C" { #include }; @@ -62,7 +63,7 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP struct EAPAuth { std::string identity; // required for all auth types std::string username; @@ -72,7 +73,7 @@ struct EAPAuth { const char *client_cert; const char *client_key; }; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP using bssid_t = std::array; @@ -82,9 +83,9 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -92,9 +93,9 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -104,9 +105,9 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP optional eap_; -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 1bccf08a7f..57c4efcdd5 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,7 +6,7 @@ #include #include -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP #include #endif #include "lwip/err.h" @@ -163,7 +163,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; } -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; } @@ -220,7 +220,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } // setup enterprise authentication if required -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = ap.get_eap().value(); @@ -264,7 +264,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); } } -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP this->wifi_apply_hostname_(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 2f6c32aec6..ad1a64d1f4 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -1,4 +1,5 @@ #include "wifi_component.h" +#include "esphome/core/macros.h" #ifdef ARDUINO_ARCH_ESP8266 @@ -6,14 +7,10 @@ #include #include -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP #include #endif -#ifdef WIFI_IS_OFF_AT_BOOT // Identifies ESP8266 Arduino 3.0.0 -#define ARDUINO_ESP8266_RELEASE_3 -#endif - extern "C" { #include "lwip/err.h" #include "lwip/dns.h" @@ -22,7 +19,7 @@ extern "C" { #if LWIP_IPV6 #include "lwip/netif.h" // struct netif #endif -#ifdef ARDUINO_ESP8266_RELEASE_3 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) #include "LwipDhcpServer.h" #define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease) #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) @@ -229,7 +226,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.bssid_set = 0; } -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { @@ -253,7 +250,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } // setup enterprise authentication if required -#ifdef ESPHOME_WIFI_WPA2_EAP +#ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = ap.get_eap().value(); @@ -296,7 +293,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret); } } -#endif // ESPHOME_WIFI_WPA2_EAP +#endif // USE_WIFI_WPA2_EAP this->wifi_apply_hostname_(); @@ -369,65 +366,75 @@ const char *get_op_mode_str(uint8_t mode) { return "UNKNOWN"; } } +// Note that this method returns PROGMEM strings, so use LOG_STR_ARG() to access them. const char *get_disconnect_reason_str(uint8_t reason) { + /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the + * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM + * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. + */ + if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 + switch (reason) { + case REASON_AUTH_EXPIRE: + return LOG_STR("Auth Expired"); + case REASON_AUTH_LEAVE: + return LOG_STR("Auth Leave"); + case REASON_ASSOC_EXPIRE: + return LOG_STR("Association Expired"); + case REASON_ASSOC_TOOMANY: + return LOG_STR("Too Many Associations"); + case REASON_NOT_AUTHED: + return LOG_STR("Not Authenticated"); + case REASON_NOT_ASSOCED: + return LOG_STR("Not Associated"); + case REASON_ASSOC_LEAVE: + return LOG_STR("Association Leave"); + case REASON_ASSOC_NOT_AUTHED: + return LOG_STR("Association not Authenticated"); + case REASON_DISASSOC_PWRCAP_BAD: + return LOG_STR("Disassociate Power Cap Bad"); + case REASON_DISASSOC_SUPCHAN_BAD: + return LOG_STR("Disassociate Supported Channel Bad"); + case REASON_IE_INVALID: + return LOG_STR("IE Invalid"); + case REASON_MIC_FAILURE: + return LOG_STR("Mic Failure"); + case REASON_4WAY_HANDSHAKE_TIMEOUT: + return LOG_STR("4-Way Handshake Timeout"); + case REASON_GROUP_KEY_UPDATE_TIMEOUT: + return LOG_STR("Group Key Update Timeout"); + case REASON_IE_IN_4WAY_DIFFERS: + return LOG_STR("IE In 4-Way Handshake Differs"); + case REASON_GROUP_CIPHER_INVALID: + return LOG_STR("Group Cipher Invalid"); + case REASON_PAIRWISE_CIPHER_INVALID: + return LOG_STR("Pairwise Cipher Invalid"); + case REASON_AKMP_INVALID: + return LOG_STR("AKMP Invalid"); + case REASON_UNSUPP_RSN_IE_VERSION: + return LOG_STR("Unsupported RSN IE version"); + case REASON_INVALID_RSN_IE_CAP: + return LOG_STR("Invalid RSN IE Cap"); + case REASON_802_1X_AUTH_FAILED: + return LOG_STR("802.1x Authentication Failed"); + case REASON_CIPHER_SUITE_REJECTED: + return LOG_STR("Cipher Suite Rejected"); + } + } + switch (reason) { - case REASON_AUTH_EXPIRE: - return "Auth Expired"; - case REASON_AUTH_LEAVE: - return "Auth Leave"; - case REASON_ASSOC_EXPIRE: - return "Association Expired"; - case REASON_ASSOC_TOOMANY: - return "Too Many Associations"; - case REASON_NOT_AUTHED: - return "Not Authenticated"; - case REASON_NOT_ASSOCED: - return "Not Associated"; - case REASON_ASSOC_LEAVE: - return "Association Leave"; - case REASON_ASSOC_NOT_AUTHED: - return "Association not Authenticated"; - case REASON_DISASSOC_PWRCAP_BAD: - return "Disassociate Power Cap Bad"; - case REASON_DISASSOC_SUPCHAN_BAD: - return "Disassociate Supported Channel Bad"; - case REASON_IE_INVALID: - return "IE Invalid"; - case REASON_MIC_FAILURE: - return "Mic Failure"; - case REASON_4WAY_HANDSHAKE_TIMEOUT: - return "4-Way Handshake Timeout"; - case REASON_GROUP_KEY_UPDATE_TIMEOUT: - return "Group Key Update Timeout"; - case REASON_IE_IN_4WAY_DIFFERS: - return "IE In 4-Way Handshake Differs"; - case REASON_GROUP_CIPHER_INVALID: - return "Group Cipher Invalid"; - case REASON_PAIRWISE_CIPHER_INVALID: - return "Pairwise Cipher Invalid"; - case REASON_AKMP_INVALID: - return "AKMP Invalid"; - case REASON_UNSUPP_RSN_IE_VERSION: - return "Unsupported RSN IE version"; - case REASON_INVALID_RSN_IE_CAP: - return "Invalid RSN IE Cap"; - case REASON_802_1X_AUTH_FAILED: - return "802.1x Authentication Failed"; - case REASON_CIPHER_SUITE_REJECTED: - return "Cipher Suite Rejected"; case REASON_BEACON_TIMEOUT: - return "Beacon Timeout"; + return LOG_STR("Beacon Timeout"); case REASON_NO_AP_FOUND: - return "AP Not Found"; + return LOG_STR("AP Not Found"); case REASON_AUTH_FAIL: - return "Authentication Failed"; + return LOG_STR("Authentication Failed"); case REASON_ASSOC_FAIL: - return "Association Failed"; + return LOG_STR("Association Failed"); case REASON_HANDSHAKE_TIMEOUT: - return "Handshake Failed"; + return LOG_STR("Handshake Failed"); case REASON_UNSPECIFIED: default: - return "Unspecified"; + return LOG_STR("Unspecified"); } } @@ -451,7 +458,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); } break; } @@ -495,7 +502,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode), @@ -580,7 +587,7 @@ bool WiFiComponent::wifi_scan_start_() { config.bssid = nullptr; config.channel = 0; config.show_hidden = 1; -#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = WIFI_SCAN_TYPE_ACTIVE; if (FIRST_SCAN) { config.scan_time.active.min = 100; @@ -659,7 +666,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return false; } -#ifdef ARDUINO_ESP8266_RELEASE_3 +#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) dhcpSoftAP.begin(&info); #endif diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 690d2f3b00..915d1c6cc2 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -42,6 +42,7 @@ void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { for (int led = it.size(); led-- > 0;) { it[led].set(Color::BLACK); } + it.schedule_show(); } void WLEDLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { @@ -134,6 +135,7 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p blank_at_ = millis() + DEFAULT_BLANK_TIME; } + it.schedule_show(); return true; } diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index f2273afa9e..74f1f9ec61 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/config.py b/esphome/config.py index 93413a009c..de261f7eba 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -333,6 +333,8 @@ def validate_config(config, command_line_substitutions): result.add_error(err) return result + CORE.raw_config = config + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: from esphome.components import substitutions @@ -348,6 +350,8 @@ def validate_config(config, command_line_substitutions): result.add_error(err) return result + CORE.raw_config = config + # 1.1. Check for REPLACEME special value try: recursive_check_replaceme(config) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3aebca81b8..fb659c41ea 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -33,7 +33,6 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE, - CONF_PACKAGES, ) from esphome.core import ( CORE, @@ -836,10 +835,11 @@ pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True) def temperature(value): + err = None try: return _temperature_c(value) - except Invalid as orig_err: # noqa - pass + except Invalid as orig_err: + err = orig_err try: kelvin = _temperature_k(value) @@ -853,7 +853,7 @@ def temperature(value): except Invalid: pass - raise orig_err # noqa + raise err _color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)") @@ -1454,11 +1454,7 @@ class OnlyWith(Optional): @property def default(self): # pylint: disable=unsupported-membership-test - if self._component in CORE.raw_config or ( - CONF_PACKAGES in CORE.raw_config - and self._component - in {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()} - ): + if self._component in CORE.raw_config: return self._default return vol.UNDEFINED @@ -1628,3 +1624,17 @@ def url(value): if not parsed.scheme or not parsed.netloc: raise Invalid("Expected a URL scheme and host") return parsed.geturl() + + +def git_ref(value): + if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + raise Invalid("Not a valid git ref") + return value + + +def source_refresh(value: str): + if value.lower() == "always": + return source_refresh("0s") + if value.lower() == "never": + return source_refresh("1000y") + return positive_time_period_seconds(value) diff --git a/esphome/const.py b/esphome/const.py index e25ba7e046..44e3c09870 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.8.0" +__version__ = "2021.9.0b1" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" @@ -69,6 +69,7 @@ CONF_ATTENUATION = "attenuation" CONF_ATTRIBUTE = "attribute" CONF_AUTH = "auth" CONF_AUTO_MODE = "auto_mode" +CONF_AUTOCONF = "autoconf" CONF_AUTOMATION_ID = "automation_id" CONF_AVAILABILITY = "availability" CONF_AWAY = "away" @@ -78,6 +79,7 @@ CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_VOLTAGE = "battery_voltage" CONF_BAUD_RATE = "baud_rate" +CONF_BEEPER = "beeper" CONF_BELOW = "below" CONF_BINARY = "binary" CONF_BINARY_SENSOR = "binary_sensor" @@ -165,6 +167,7 @@ CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr" CONF_DEBOUNCE = "debounce" +CONF_DECAY_MODE = "decay_mode" CONF_DECELERATION = "deceleration" CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" @@ -234,12 +237,14 @@ CONF_FAN_WITH_COOLING = "fan_with_cooling" CONF_FAN_WITH_HEATING = "fan_with_heating" CONF_FAST_CONNECT = "fast_connect" CONF_FILE = "file" +CONF_FILES = "files" CONF_FILTER = "filter" CONF_FILTER_OUT = "filter_out" CONF_FILTERS = "filters" CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" +CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" @@ -277,6 +282,9 @@ CONF_HUMIDITY = "humidity" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" +CONF_IBEACON_MAJOR = "ibeacon_major" +CONF_IBEACON_MINOR = "ibeacon_minor" +CONF_IBEACON_UUID = "ibeacon_uuid" CONF_ICON = "icon" CONF_ID = "id" CONF_IDENTITY = "identity" @@ -317,7 +325,6 @@ CONF_KEY = "key" CONF_LAMBDA = "lambda" CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_FINGER_ID = "last_finger_id" -CONF_LAST_RESET_TYPE = "last_reset_type" CONF_LATITUDE = "latitude" CONF_LENGTH = "length" CONF_LEVEL = "level" @@ -420,6 +427,7 @@ CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" CONF_ON_SHUTDOWN = "on_shutdown" +CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_STATE = "on_state" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" @@ -506,6 +514,8 @@ CONF_PROTOCOL = "protocol" CONF_PULL_MODE = "pull_mode" CONF_PULSE_LENGTH = "pulse_length" CONF_QOS = "qos" +CONF_RADON = "radon" +CONF_RADON_LONG_TERM = "radon_long_term" CONF_RANDOM = "random" CONF_RANGE = "range" CONF_RANGE_FROM = "range_from" @@ -518,8 +528,10 @@ CONF_REACTIVE_POWER = "reactive_power" CONF_REBOOT_TIMEOUT = "reboot_timeout" CONF_RECEIVE_TIMEOUT = "receive_timeout" CONF_RED = "red" +CONF_REF = "ref" CONF_REFERENCE_RESISTANCE = "reference_resistance" CONF_REFERENCE_TEMPERATURE = "reference_temperature" +CONF_REFRESH = "refresh" CONF_REPEAT = "repeat" CONF_REPOSITORY = "repository" CONF_RESET_PIN = "reset_pin" @@ -605,6 +617,10 @@ 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_SUPPORTED_FAN_MODES = "supported_fan_modes" +CONF_SUPPORTED_MODES = "supported_modes" +CONF_SUPPORTED_PRESETS = "supported_presets" +CONF_SUPPORTED_SWING_MODES = "supported_swing_modes" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" CONF_SWING_BOTH_ACTION = "swing_both_action" @@ -731,6 +747,7 @@ ICON_PERCENT = "mdi:percent" ICON_POWER = "mdi:power" ICON_PULSE = "mdi:pulse" ICON_RADIATOR = "mdi:radiator" +ICON_RADIOACTIVE = "mdi:radioactive" ICON_RESTART = "mdi:restart" ICON_ROTATE_RIGHT = "mdi:rotate-right" ICON_RULER = "mdi:ruler" @@ -752,6 +769,7 @@ ICON_WEATHER_WINDY = "mdi:weather-windy" ICON_WIFI = "mdi:wifi" UNIT_AMPERE = "A" +UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³" UNIT_CELSIUS = "°C" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" @@ -768,6 +786,8 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" @@ -798,7 +818,6 @@ DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_CONNECTIVITY = "connectivity" DEVICE_CLASS_DOOR = "door" DEVICE_CLASS_GARAGE_DOOR = "garage_door" -DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_LIGHT = "light" DEVICE_CLASS_LOCK = "lock" @@ -813,25 +832,37 @@ DEVICE_CLASS_PROBLEM = "problem" DEVICE_CLASS_SAFETY = "safety" DEVICE_CLASS_SMOKE = "smoke" DEVICE_CLASS_SOUND = "sound" +DEVICE_CLASS_UPDATE = "update" DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component DEVICE_CLASS_EMPTY = "" DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_POWER = "power" # device classes of sensor component -DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" +DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" +DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" 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_NITROGEN_DIOXIDE = "nitrogen_dioxide" +DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide" +DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide" +DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PM1 = "pm1" +DEVICE_CLASS_PM10 = "pm10" +DEVICE_CLASS_PM25 = "pm25" DEVICE_CLASS_POWER_FACTOR = "power_factor" DEVICE_CLASS_PRESSURE = "pressure" +DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" +DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide" +DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" +DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLTAGE = "voltage" # state classes @@ -842,10 +873,3 @@ STATE_CLASS_MEASUREMENT = "measurement" # The state represents a total that only increases, a decrease is considered a reset. STATE_CLASS_TOTAL_INCREASING = "total_increasing" - -# This sensor does not support resetting. ie, it is not accumulative -LAST_RESET_TYPE_NONE = "" -# This sensor is expected to never reset its value -LAST_RESET_TYPE_NEVER = "never" -# This sensor may reset and Home Assistant will watch for this -LAST_RESET_TYPE_AUTO = "auto" diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1a3158e4ce..fac17a8271 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -19,7 +19,7 @@ void Application::register_component_(Component *comp) { for (auto *c : this->components_) { if (comp == c) { - ESP_LOGW(TAG, "Component already registered! (%p)", c); + ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c); return; } } @@ -66,23 +66,19 @@ void Application::setup() { } void Application::loop() { uint32_t new_app_state = 0; - const uint32_t start = millis(); this->scheduler.call(); for (Component *component : this->looping_components_) { - component->call(); + { + WarnIfComponentBlockingGuard guard{component}; + component->call(); + } new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; this->feed_wdt(); } this->app_state_ = new_app_state; - const uint32_t end = millis(); - if (end - start > 200) { - ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f); - ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop()."); - } - const uint32_t now = millis(); if (HighFrequencyLoopRequester::is_high_frequency()) { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f6b15b1977..cd3081998b 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -20,6 +20,7 @@ const float PROCESSOR = 400.0; const float BLUETOOTH = 350.0f; const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; +const float BEFORE_CONNECTION = 220.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; const float LATE = -100.0f; @@ -92,8 +93,13 @@ void Component::call() { break; } } +const char *Component::get_component_source() const { + if (this->component_source_ == nullptr) + return ""; + return this->component_source_; +} void Component::mark_failed() { - ESP_LOGE(TAG, "Component was marked as failed."); + ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); @@ -190,4 +196,18 @@ 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; } +WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) { + component_ = component; + started_ = millis(); +} +WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() { + uint32_t now = millis(); + if (now - started_ > 50) { + const char *src = component_ == nullptr ? "" : component_->get_component_source(); + ESP_LOGV(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f); + ESP_LOGV(TAG, "Components should block for at most 20-30ms."); + ; + } +} + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index a4a945ef2a..ea87ebcdfe 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -29,6 +29,8 @@ extern const float PROCESSOR; extern const float BLUETOOTH; extern const float AFTER_BLUETOOTH; extern const float WIFI; +/// For components that should be initialized after WiFi and before API is connected. +extern const float BEFORE_CONNECTION; /// For components that should be initialized after WiFi is connected. extern const float AFTER_WIFI; /// For components that should be initialized after a data connection (API/MQTT) is connected. @@ -38,8 +40,12 @@ extern const float LATE; } // namespace setup_priority +static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; + #define LOG_UPDATE_INTERVAL(this) \ - if (this->get_update_interval() < 100) { \ + if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \ + ESP_LOGCONFIG(TAG, " Update Interval: never"); \ + } else if (this->get_update_interval() < 100) { \ ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ } else { \ ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ @@ -130,6 +136,17 @@ class Component { bool has_overridden_loop() const; + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core, and should not be called manually. + */ + void set_component_source(const char *source) { component_source_ = source; } + /** Get the integration where this component was declared as a string. + * + * Returns "" if source not set + */ + const char *get_component_source() const; + protected: virtual void call_loop(); virtual void call_setup(); @@ -201,6 +218,7 @@ class Component { uint32_t component_state_{0x0000}; ///< State of this component. float setup_priority_override_{NAN}; + const char *component_source_ = nullptr; }; /** This class simplifies creating components that periodically check a state. @@ -276,4 +294,14 @@ class Nameable { bool disabled_by_default_{false}; }; +class WarnIfComponentBlockingGuard { + public: + WarnIfComponentBlockingGuard(Component *component); + ~WarnIfComponentBlockingGuard(); + + protected: + uint32_t started_; + Component *component_; +}; + } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5c176d1b33..3cca6445b5 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -1,31 +1,61 @@ #pragma once -// This file is auto-generated! Do not edit! +// This file is not used by the runtime, instead, a version is generated during +// compilation with only the relevant feature flags for the current build. +// +// This file is only used by static analyzers and IDEs. + +// Informative flags +#define ESPHOME_BOARD "dummy_board" +#define ESPHOME_PROJECT_NAME "dummy project" +#define ESPHOME_PROJECT_VERSION "v2" + +// Feature flags +#define USE_ADC_SENSOR_VCC #define USE_API -#define USE_LOGGER #define USE_BINARY_SENSOR -#define USE_SENSOR -#define USE_SWITCH -#define USE_WIFI -#define USE_STATUS_LED -#define USE_TEXT_SENSOR -#define USE_FAN -#define USE_COVER -#define USE_LIGHT +#define USE_CAPTIVE_PORTAL #define USE_CLIMATE -#define USE_NUMBER -#define USE_SELECT -#define USE_MQTT -#define USE_POWER_SUPPLY +#define USE_COVER +#define USE_DEEP_SLEEP +#define USE_ESP8266_PREFERENCES_FLASH +#define USE_FAN #define USE_HOMEASSISTANT_TIME +#define USE_I2C_MULTIPLEXER #define USE_JSON +#define USE_LIGHT +#define USE_LOGGER +#define USE_MDNS +#define USE_MQTT +#define USE_NUMBER +#define USE_OTA_STATE_CALLBACK +#define USE_POWER_SUPPLY +#define USE_PROMETHEUS +#define USE_SELECT +#define USE_SENSOR +#define USE_STATUS_LED +#define USE_SWITCH +#define USE_TEXT_SENSOR +#define USE_TFT_UPLOAD +#define USE_TIME +#define USE_WIFI +#define USE_WIFI_WPA2_EAP + #ifdef ARDUINO_ARCH_ESP32 -#define USE_ESP32_CAMERA #define USE_ESP32_BLE_SERVER +#define USE_ESP32_CAMERA +#define USE_ETHERNET #define USE_IMPROV #endif -#define USE_TIME -#define USE_DEEP_SLEEP -#define USE_CAPTIVE_PORTAL -#define ESPHOME_BOARD "dummy_board" -#define USE_MDNS + +#ifdef ARDUINO_ARCH_ESP8266 +#define USE_SOCKET_IMPL_LWIP_TCP +#else +#define USE_SOCKET_IMPL_BSD_SOCKETS +#endif + +#define USE_API_PLAINTEXT +#define USE_API_NOISE + +// Disabled feature flags +//#define USE_BSEC // Requires a library with proprietary license. diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index 6b8350991e..7851ba01b6 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -1,4 +1,5 @@ #include "esphome/core/esphal.h" +#include "esphome/core/macros.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -298,7 +299,7 @@ void force_link_symbols() { } // namespace esphome -#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) // Fix 2.3.0 std missing memchr extern "C" { void *memchr(const void *s, int c, size_t n) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9e9c775899..c5ff0102c3 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -55,6 +55,15 @@ double random_double() { return random_uint32() / double(UINT32_MAX); } float random_float() { return float(random_double()); } +void fill_random(uint8_t *data, size_t len) { +#ifdef ARDUINO_ARCH_ESP32 + esp_fill_random(data, len); +#else + int err = os_get_random(data, len); + assert(err == 0); +#endif +} + static uint32_t fast_random_seed = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5868918cd6..60bc7a9ad3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -109,6 +109,8 @@ double random_double(); /// Returns a random float between 0 and 1. Essentially just casts random_double() to a float. float random_float(); +void fill_random(uint8_t *data, size_t len); + void fast_random_set_seed(uint32_t seed); uint32_t fast_random_32(); uint16_t fast_random_16(); diff --git a/esphome/core/log.h b/esphome/core/log.h index 0eec28101f..fbaaf14408 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -7,6 +7,7 @@ #include "WString.h" #endif +#include "esphome/core/macros.h" // avoid esp-idf redefining our macros #include "esphome/core/esphal.h" @@ -162,4 +163,28 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #define ONOFF(b) ((b) ? "ON" : "OFF") #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE") +#ifdef USE_STORE_LOG_STR_IN_FLASH +#define LOG_STR(s) PSTR(s) + +// From Arduino 2.5 onwards, we can pass a PSTR() to printf(). For previous versions, emulate support +// by copying the message to a local buffer first. String length is limited to 63 characters. +// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31 +#include +#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 5, 0) +#define LOG_STR_ARG(s) \ + ({ \ + char __buf[64]; \ + __buf[63] = '\0'; \ + strncpy_P(__buf, s, 63); \ + __buf; \ + }) +#else +#define LOG_STR_ARG(s) (s) +#endif + +#else +#define LOG_STR(s) (s) +#define LOG_STR_ARG(s) (s) +#endif + } // namespace esphome diff --git a/esphome/core/macros.h b/esphome/core/macros.h new file mode 100644 index 0000000000..59b52bf7a1 --- /dev/null +++ b/esphome/core/macros.h @@ -0,0 +1,56 @@ +#pragma once + +#define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch)) + +#if defined(ARDUINO_ARCH_ESP8266) + +#include +#if defined(ARDUINO_ESP8266_MAJOR) && defined(ARDUINO_ESP8266_MINOR) && defined(ARDUINO_ESP8266_REVISION) // v3.0.1+ +#define ARDUINO_VERSION_CODE VERSION_CODE(ARDUINO_ESP8266_MAJOR, ARDUINO_ESP8266_MINOR, ARDUINO_ESP8266_REVISION) +#elif ARDUINO_ESP8266_GIT_VER == 0xefb0341a // version defines were screwed up in v3.0.0 +#define ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_4) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 4) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_3) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 3) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_7_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_3) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 3) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_6_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_5_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 2) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_1) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 1) +#elif defined(ARDUINO_ESP8266_RELEASE_2_4_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 0) +#elif defined(ARDUINO_ESP8266_RELEASE_2_3_0) +#define ARDUINO_VERSION_CODE VERSION_CODE(2, 3, 0) +#else +#warning "Could not determine Arduino framework version, update esphome/core/macros.h!" +#endif + +#elif defined(ARDUINO_ARCH_ESP32) + +#if defined(IDF_VER) // identifies v2, needed since v1 doesn't have the esp_arduino_version.h header +#include +#define ARDUINO_VERSION_CODE \ + VERSION_CODE(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATH) +#else +#define ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 0) // there are no defines identifying minor/patch version +#endif + +#endif diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 410c68052f..5718e3b396 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -7,7 +7,6 @@ namespace esphome { static const char *const TAG = "scheduler"; -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler @@ -155,7 +154,10 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() { // Warning: During f(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - item->f(); + { + WarnIfComponentBlockingGuard guard{item->component}; + item->f(); + } } { diff --git a/esphome/core/version.h b/esphome/core/version.h index 0942c3e52f..b64f581b25 100644 --- a/esphome/core/version.h +++ b/esphome/core/version.h @@ -1,3 +1,9 @@ #pragma once -// This file is auto-generated! Do not edit! + +// This file is not used by the runtime, instead, a version is generated during +// compilation with only the version for the current build. This is kept in its +// own file so that not all files have to be recompiled for each new release. +// +// This file is only used by static analyzers and IDEs. + #define ESPHOME_VERSION "dev" diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 1d66eabf6c..7912e4ae06 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,3 +1,5 @@ +import logging + from esphome.const import ( CONF_INVERTED, CONF_MODE, @@ -15,6 +17,9 @@ from esphome.cpp_types import App, GPIOPin from esphome.util import Registry, RegistryEntry +_LOGGER = logging.getLogger(__name__) + + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. @@ -42,6 +47,8 @@ async def register_component(var, config): :param var: The variable representing the component. :param config: The configuration for the component. """ + import inspect + id_ = str(var.base) if id_ not in CORE.component_ids: raise ValueError( @@ -54,6 +61,32 @@ async def register_component(var, config): add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + + # Set component source by inspecting the stack and getting the callee module + # https://stackoverflow.com/a/1095621 + name = None + try: + for frm in inspect.stack()[1:]: + mod = inspect.getmodule(frm[0]) + if mod is None: + continue + name = mod.__name__ + if name.startswith("esphome.components."): + name = name[len("esphome.components.") :] + break + if name == "esphome.automation": + name = "automation" + # continue looking further up in stack in case we find a better one + if name == "esphome.coroutine": + # Only works for async-await coroutine syntax + break + except (KeyError, AttributeError, IndexError) as e: + _LOGGER.warning( + "Error while finding name of component, please report this", exc_info=e + ) + if name is not None: + add(var.set_component_source(name)) + add(App.register_component(var)) return var diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index ea55ea3b18..97f9d60693 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -41,7 +41,7 @@ from .util import password_hash # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa -from esphome.zeroconf import DashboardStatus, Zeroconf +from esphome.zeroconf import DashboardStatus, EsphomeZeroconf _LOGGER = logging.getLogger(__name__) @@ -431,7 +431,7 @@ class DashboardEntry: @property def name(self): if self.storage is None: - return self.filename[: -len(".yaml")] + return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name @property @@ -501,7 +501,7 @@ def _ping_func(filename, address): class MDNSStatusThread(threading.Thread): def run(self): - zc = Zeroconf() + zc = EsphomeZeroconf() def on_update(dat): for key, b in dat.items(): @@ -600,7 +600,7 @@ class EditRequestHandler(BaseHandler): content = "" if os.path.isfile(filename): # pylint: disable=no-value-for-parameter - with open(filename, "r") as f: + with open(file=filename, mode="r", encoding="utf-8") as f: content = f.read() self.write(content) @@ -608,7 +608,7 @@ class EditRequestHandler(BaseHandler): @bind_config def post(self, configuration=None): # pylint: disable=no-value-for-parameter - with open(settings.rel_path(configuration), "wb") as f: + with open(file=settings.rel_path(configuration), mode="wb") as f: f.write(self.request.body) self.set_status(200) diff --git a/esphome/git.py b/esphome/git.py new file mode 100644 index 0000000000..12c6b41648 --- /dev/null +++ b/esphome/git.py @@ -0,0 +1,74 @@ +from pathlib import Path +import subprocess +import hashlib +import logging + +from datetime import datetime + +from esphome.core import CORE, TimePeriodSeconds +import esphome.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + + +def run_git_command(cmd, cwd=None): + try: + ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) + except FileNotFoundError as err: + raise cv.Invalid( + "git is not installed but required for external_components.\n" + "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" + ) from err + + if ret.returncode != 0 and ret.stderr: + err_str = ret.stderr.decode("utf-8") + lines = [x.strip() for x in err_str.splitlines()] + if lines[-1].startswith("fatal:"): + raise cv.Invalid(lines[-1][len("fatal: ") :]) + raise cv.Invalid(err_str) + + +def _compute_destination_path(key: str, domain: str) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / domain + h = hashlib.new("sha256") + h.update(key.encode()) + return base_dir / h.hexdigest()[:8] + + +def clone_or_update( + *, url: str, ref: str = None, refresh: TimePeriodSeconds, domain: str +) -> Path: + key = f"{url}@{ref}" + repo_dir = _compute_destination_path(key, domain) + if not repo_dir.is_dir(): + _LOGGER.info("Cloning %s", key) + _LOGGER.debug("Location: %s", repo_dir) + cmd = ["git", "clone", "--depth=1"] + if ref is not None: + cmd += ["--branch", ref] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) + + else: + # Check refresh needed + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") + # On first clone, FETCH_HEAD does not exists + if not file_timestamp.exists(): + file_timestamp = Path(repo_dir / ".git" / "HEAD") + age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime) + if age.total_seconds() > refresh.total_seconds: + _LOGGER.info("Updating %s", key) + _LOGGER.debug("Location: %s", repo_dir) + # Stash local changes (if any) + run_git_command( + ["git", "stash", "push", "--include-untracked"], str(repo_dir) + ) + # Fetch remote ref + cmd = ["git", "fetch", "--", "origin"] + if ref is not None: + cmd.append(ref) + run_git_command(cmd, str(repo_dir)) + # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) + run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + + return repo_dir diff --git a/esphome/helpers.py b/esphome/helpers.py index ad7b8272b2..a1cb4367c5 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -97,10 +97,10 @@ def is_ip_address(host): def _resolve_with_zeroconf(host): from esphome.core import EsphomeError - from esphome.zeroconf import Zeroconf + from esphome.zeroconf import EsphomeZeroconf try: - zc = Zeroconf() + zc = EsphomeZeroconf() except Exception as err: raise EsphomeError( "Cannot start mDNS sockets, is this a docker container without " @@ -276,11 +276,11 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: # A dict of types that need to be converted to heaptypes before a class can be added # to the object _TYPE_OVERLOADS = { - int: type("EInt", (int,), dict()), - float: type("EFloat", (float,), dict()), - str: type("EStr", (str,), dict()), - dict: type("EDict", (str,), dict()), - list: type("EList", (list,), dict()), + int: type("EInt", (int,), {}), + float: type("EFloat", (float,), {}), + str: type("EStr", (str,), {}), + dict: type("EDict", (str,), {}), + list: type("EList", (list,), {}), } # cache created classes here diff --git a/esphome/util.py b/esphome/util.py index 56bc97ca71..527e370ad8 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -260,8 +260,8 @@ def filter_yaml_files(files): f for f in files if ( - os.path.splitext(f)[1] == ".yaml" - and os.path.basename(f) != "secrets.yaml" + os.path.splitext(f)[1] in (".yaml", ".yml") + and os.path.basename(f) not in ("secrets.yaml", "secrets.yml") and not os.path.basename(f).startswith(".") ) ] diff --git a/esphome/writer.py b/esphome/writer.py index 641ae9b3cc..09ed284173 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -481,5 +481,5 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome def write_gitignore(): path = CORE.relative_config_path(".gitignore") if not os.path.isfile(path): - with open(path, "w") as f: + with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index a44c7c9114..e94b59d3ae 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,520 +1,27 @@ -# Custom zeroconf implementation based on python-zeroconf -# (https://github.com/jstasiak/python-zeroconf) that supports Python 2 - -import errno -import logging -import select import socket -import struct -import sys import threading import time - -import ifaddr - -log = logging.getLogger(__name__) - -# Some timing constants - -_LISTENER_TIME = 200 - -# Some DNS constants - -_MDNS_ADDR = "224.0.0.251" -_MDNS_PORT = 5353 - -_MAX_MSG_ABSOLUTE = 8966 - -_FLAGS_QR_MASK = 0x8000 # query response mask -_FLAGS_QR_QUERY = 0x0000 # query -_FLAGS_QR_RESPONSE = 0x8000 # response - -_FLAGS_AA = 0x0400 # Authoritative answer -_FLAGS_TC = 0x0200 # Truncated -_FLAGS_RD = 0x0100 # Recursion desired -_FLAGS_RA = 0x8000 # Recursion available - -_FLAGS_Z = 0x0040 # Zero -_FLAGS_AD = 0x0020 # Authentic data -_FLAGS_CD = 0x0010 # Checking disabled - -_CLASS_IN = 1 -_CLASS_CS = 2 -_CLASS_CH = 3 -_CLASS_HS = 4 -_CLASS_NONE = 254 -_CLASS_ANY = 255 -_CLASS_MASK = 0x7FFF -_CLASS_UNIQUE = 0x8000 - -_TYPE_A = 1 -_TYPE_NS = 2 -_TYPE_MD = 3 -_TYPE_MF = 4 -_TYPE_CNAME = 5 -_TYPE_SOA = 6 -_TYPE_MB = 7 -_TYPE_MG = 8 -_TYPE_MR = 9 -_TYPE_NULL = 10 -_TYPE_WKS = 11 -_TYPE_PTR = 12 -_TYPE_HINFO = 13 -_TYPE_MINFO = 14 -_TYPE_MX = 15 -_TYPE_TXT = 16 -_TYPE_AAAA = 28 -_TYPE_SRV = 33 -_TYPE_ANY = 255 - -# Mapping constants to names -int2byte = struct.Struct(">B").pack - - -# Exceptions -class Error(Exception): - pass - - -class IncomingDecodeError(Error): - pass - - -# pylint: disable=no-init -class QuietLogger: - _seen_logs = {} - - @classmethod - def log_exception_warning(cls, logger_data=None): - exc_info = sys.exc_info() - exc_str = str(exc_info[1]) - if exc_str not in cls._seen_logs: - # log at warning level the first time this is seen - cls._seen_logs[exc_str] = exc_info - logger = log.warning - else: - logger = log.debug - if logger_data is not None: - logger(*logger_data) - logger("Exception occurred:", exc_info=True) - - @classmethod - def log_warning_once(cls, *args): - msg_str = args[0] - if msg_str not in cls._seen_logs: - cls._seen_logs[msg_str] = 0 - logger = log.warning - else: - logger = log.debug - cls._seen_logs[msg_str] += 1 - logger(*args) - - -class DNSEntry: - """A DNS entry""" - - def __init__(self, name, type_, class_): - self.key = name.lower() - self.name = name - self.type = type_ - self.class_ = class_ & _CLASS_MASK - self.unique = (class_ & _CLASS_UNIQUE) != 0 - - -class DNSQuestion(DNSEntry): - """A DNS question entry""" - - def __init__(self, name, type_, class_): - DNSEntry.__init__(self, name, type_, class_) - - def answered_by(self, rec): - """Returns true if the question is answered by the record""" - return ( - self.class_ == rec.class_ - and (self.type == rec.type or self.type == _TYPE_ANY) - and self.name == rec.name - ) - - -class DNSRecord(DNSEntry): - """A DNS record - like a DNS entry, but has a TTL""" - - def __init__(self, name, type_, class_, ttl): - DNSEntry.__init__(self, name, type_, class_) - self.ttl = 15 - self.created = time.time() - - def write(self, out): - """Abstract method""" - raise NotImplementedError - - def is_expired(self, now): - return self.created + self.ttl <= now - - def is_removable(self, now): - return self.created + self.ttl * 2 <= now - - -class DNSAddress(DNSRecord): - """A DNS address record""" - - def __init__(self, name, type_, class_, ttl, address): - DNSRecord.__init__(self, name, type_, class_, ttl) - self.address = address - - def write(self, out): - """Used in constructing an outgoing packet""" - out.write_string(self.address) - - -class DNSText(DNSRecord): - """A DNS text record""" - - def __init__(self, name, type_, class_, ttl, text): - assert isinstance(text, (bytes, type(None))) - DNSRecord.__init__(self, name, type_, class_, ttl) - self.text = text - - def write(self, out): - """Used in constructing an outgoing packet""" - out.write_string(self.text) - - -class DNSIncoming(QuietLogger): - """Object representation of an incoming DNS packet""" - - def __init__(self, data): - """Constructor from string holding bytes of packet""" - self.offset = 0 - self.data = data - self.questions = [] - self.answers = [] - self.id = 0 - self.flags = 0 # type: int - self.num_questions = 0 - self.num_answers = 0 - self.num_authorities = 0 - self.num_additionals = 0 - self.valid = False - - try: - self.read_header() - self.read_questions() - self.read_others() - self.valid = True - - except (IndexError, struct.error, IncomingDecodeError): - self.log_exception_warning( - ("Choked at offset %d while unpacking %r", self.offset, data) - ) - - def unpack(self, format_): - length = struct.calcsize(format_) - info = struct.unpack(format_, self.data[self.offset : self.offset + length]) - self.offset += length - return info - - def read_header(self): - """Reads header portion of packet""" - ( - self.id, - self.flags, - self.num_questions, - self.num_answers, - self.num_authorities, - self.num_additionals, - ) = self.unpack(b"!6H") - - def read_questions(self): - """Reads questions section of packet""" - for _ in range(self.num_questions): - name = self.read_name() - type_, class_ = self.unpack(b"!HH") - - question = DNSQuestion(name, type_, class_) - self.questions.append(question) - - def read_character_string(self): - """Reads a character string from the packet""" - length = self.data[self.offset] - self.offset += 1 - return self.read_string(length) - - def read_string(self, length): - """Reads a string of a given length from the packet""" - info = self.data[self.offset : self.offset + length] - self.offset += length - return info - - def read_unsigned_short(self): - """Reads an unsigned short from the packet""" - return self.unpack(b"!H")[0] - - def read_others(self): - """Reads the answers, authorities and additionals section of the - packet""" - n = self.num_answers + self.num_authorities + self.num_additionals - for _ in range(n): - domain = self.read_name() - type_, class_, ttl, length = self.unpack(b"!HHiH") - - rec = None - if type_ == _TYPE_A: - rec = DNSAddress(domain, type_, class_, ttl, self.read_string(4)) - elif type_ == _TYPE_TXT: - rec = DNSText(domain, type_, class_, ttl, self.read_string(length)) - elif type_ == _TYPE_AAAA: - rec = DNSAddress(domain, type_, class_, ttl, self.read_string(16)) - else: - # Try to ignore types we don't know about - # Skip the payload for the resource record so the next - # records can be parsed correctly - self.offset += length - - if rec is not None: - self.answers.append(rec) - - def is_query(self): - """Returns true if this is a query""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY - - def is_response(self): - """Returns true if this is a response""" - return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE - - def read_utf(self, offset, length): - """Reads a UTF-8 string of a given length from the packet""" - return str(self.data[offset : offset + length], "utf-8", "replace") - - def read_name(self): - """Reads a domain name from the packet""" - result = "" - off = self.offset - next_ = -1 - first = off - - while True: - length = self.data[off] - off += 1 - if length == 0: - break - t = length & 0xC0 - if t == 0x00: - result = "".join((result, self.read_utf(off, length) + ".")) - off += length - elif t == 0xC0: - if next_ < 0: - next_ = off + 1 - off = ((length & 0x3F) << 8) | self.data[off] - if off >= first: - raise IncomingDecodeError(f"Bad domain name (circular) at {off}") - first = off - else: - raise IncomingDecodeError(f"Bad domain name at {off}") - - if next_ >= 0: - self.offset = next_ - else: - self.offset = off - - return result - - -class DNSOutgoing: - """Object representation of an outgoing packet""" - - def __init__(self, flags): - self.finished = False - self.id = 0 - self.flags = flags - self.names = {} - self.data = [] - self.size = 12 - self.state = False - - self.questions = [] - self.answers = [] - - def add_question(self, record): - """Adds a question""" - self.questions.append(record) - - def pack(self, format_, value): - self.data.append(struct.pack(format_, value)) - self.size += struct.calcsize(format_) - - def write_byte(self, value): - """Writes a single byte to the packet""" - self.pack(b"!c", int2byte(value)) - - def insert_short(self, index, value): - """Inserts an unsigned short in a certain position in the packet""" - self.data.insert(index, struct.pack(b"!H", value)) - self.size += 2 - - def write_short(self, value): - """Writes an unsigned short to the packet""" - self.pack(b"!H", value) - - def write_int(self, value): - """Writes an unsigned integer to the packet""" - self.pack(b"!I", int(value)) - - def write_string(self, value): - """Writes a string to the packet""" - assert isinstance(value, bytes) - self.data.append(value) - self.size += len(value) - - def write_utf(self, s): - """Writes a UTF-8 string of a given length to the packet""" - utfstr = s.encode("utf-8") - length = len(utfstr) - self.write_byte(length) - self.write_string(utfstr) - - def write_character_string(self, value): - assert isinstance(value, bytes) - length = len(value) - self.write_byte(length) - self.write_string(value) - - def write_name(self, name): - # split name into each label - parts = name.split(".") - if not parts[-1]: - parts.pop() - - # construct each suffix - name_suffices = [".".join(parts[i:]) for i in range(len(parts))] - - # look for an existing name or suffix - for count, sub_name in enumerate(name_suffices): - if sub_name in self.names: - break - else: - count = len(name_suffices) - - # note the new names we are saving into the packet - name_length = len(name.encode("utf-8")) - for suffix in name_suffices[:count]: - self.names[suffix] = ( - self.size + name_length - len(suffix.encode("utf-8")) - 1 - ) - - # write the new names out. - for part in parts[:count]: - self.write_utf(part) - - # if we wrote part of the name, create a pointer to the rest - if count != len(name_suffices): - # Found substring in packet, create pointer - index = self.names[name_suffices[count]] - self.write_byte((index >> 8) | 0xC0) - self.write_byte(index & 0xFF) - else: - # this is the end of a name - self.write_byte(0) - - def write_question(self, question): - self.write_name(question.name) - self.write_short(question.type) - self.write_short(question.class_) - - def packet(self): - if not self.state: - for question in self.questions: - self.write_question(question) - self.state = True - - self.insert_short(0, 0) # num additionals - self.insert_short(0, 0) # num authorities - self.insert_short(0, 0) # num answers - self.insert_short(0, len(self.questions)) - self.insert_short(0, self.flags) # _FLAGS_QR_QUERY - self.insert_short(0, 0) - return b"".join(self.data) - - -class Engine(threading.Thread): - def __init__(self, zc): - threading.Thread.__init__(self, name="zeroconf-Engine") - self.daemon = True - self.zc = zc - self.readers = {} - self.timeout = 5 - self.condition = threading.Condition() - self.start() - - def run(self): - while not self.zc.done: - # pylint: disable=len-as-condition - with self.condition: - rs = self.readers.keys() - if len(rs) == 0: - # No sockets to manage, but we wait for the timeout - # or addition of a socket - self.condition.wait(self.timeout) - - if len(rs) != 0: - try: - rr, _, _ = select.select(rs, [], [], self.timeout) - if not self.zc.done: - for socket_ in rr: - reader = self.readers.get(socket_) - if reader: - reader.handle_read(socket_) - - except OSError as e: - # If the socket was closed by another thread, during - # shutdown, ignore it and exit - if e.args[0] != socket.EBADF or not self.zc.done: - raise - - def add_reader(self, reader, socket_): - with self.condition: - self.readers[socket_] = reader - self.condition.notify() - - def del_reader(self, socket_): - with self.condition: - del self.readers[socket_] - self.condition.notify() - - -class Listener(QuietLogger): - def __init__(self, zc): - self.zc = zc - self.data = None - - def handle_read(self, socket_): - try: - data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE) - except Exception: # pylint: disable=broad-except - self.log_exception_warning() - return - - log.debug("Received from %r:%r: %r ", addr, port, data) - - self.data = data - msg = DNSIncoming(data) - if not msg.valid or msg.is_query(): - pass - else: - self.zc.handle_response(msg) - - -class RecordUpdateListener: - def update_record(self, zc, now, record): - raise NotImplementedError() +from typing import Dict, Optional + +from zeroconf import ( + _CLASS_IN, + _FLAGS_QR_QUERY, + _TYPE_A, + DNSAddress, + DNSOutgoing, + DNSRecord, + DNSQuestion, + RecordUpdateListener, + Zeroconf, +) class HostResolver(RecordUpdateListener): - def __init__(self, name): + def __init__(self, name: str): self.name = name - self.address = None + self.address: Optional[bytes] = None - def update_record(self, zc, now, record): + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: if record is None: return if record.type == _TYPE_A: @@ -522,14 +29,14 @@ class HostResolver(RecordUpdateListener): if record.name == self.name: self.address = record.address - def request(self, zc, timeout): + def request(self, zc: Zeroconf, timeout: float) -> bool: now = time.time() delay = 0.2 next_ = now + delay last = now + timeout try: - zc.add_listener(self) + zc.add_listener(self, None) while self.address is None: if last <= now: # Timeout @@ -550,56 +57,52 @@ class HostResolver(RecordUpdateListener): class DashboardStatus(RecordUpdateListener, threading.Thread): - def __init__(self, zc, on_update): + PING_AFTER = 15 * 1000 # Send new mDNS request after 15 seconds + OFFLINE_AFTER = PING_AFTER * 2 # Offline if no mDNS response after 30 seconds + + def __init__(self, zc: Zeroconf, on_update) -> None: threading.Thread.__init__(self) self.zc = zc - self.query_hosts = set() - self.key_to_host = {} - self.cache = {} + self.query_hosts: set[str] = set() + self.key_to_host: Dict[str, str] = {} self.stop_event = threading.Event() self.query_event = threading.Event() self.on_update = on_update - def update_record(self, zc, now, record): - if record is None: - return - if record.type in (_TYPE_A, _TYPE_AAAA, _TYPE_TXT): - assert isinstance(record, DNSEntry) - if record.name in self.query_hosts: - self.cache.setdefault(record.name, []).insert(0, record) - self.purge_cache() + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: + pass - def purge_cache(self): - new_cache = {} - for host, records in self.cache.items(): - if host not in self.query_hosts: - continue - new_records = [rec for rec in records if not rec.is_removable(time.time())] - if new_records: - new_cache[host] = new_records - self.cache = new_cache - self.on_update({key: self.host_status(key) for key in self.key_to_host}) - - def request_query(self, hosts): + def request_query(self, hosts: Dict[str, str]) -> None: self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() - def stop(self): + def stop(self) -> None: self.stop_event.set() self.query_event.set() - def host_status(self, key): - return self.key_to_host.get(key) in self.cache + def host_status(self, key: str) -> bool: + entries = self.zc.cache.entries_with_name(key) + if not entries: + return False + now = time.time() * 1000 - def run(self): - self.zc.add_listener(self) + return any( + (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries + ) + + def run(self) -> None: + self.zc.add_listener(self, None) while not self.stop_event.is_set(): - self.purge_cache() + self.on_update( + {key: self.host_status(host) for key, host in self.key_to_host.items()} + ) + now = time.time() * 1000 for host in self.query_hosts: - if all( - record.is_expired(time.time()) - for record in self.cache.get(host, []) + entries = self.zc.cache.entries_with_name(host) + if not entries or all( + (entry.created + DashboardStatus.PING_AFTER) <= now + for entry in entries ): out = DNSOutgoing(_FLAGS_QR_QUERY) out.add_question(DNSQuestion(host, _TYPE_A, _CLASS_IN)) @@ -609,186 +112,9 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): self.zc.remove_listener(self) -def get_all_addresses(): - return list( - { - addr.ip - for iface in ifaddr.get_adapters() - for addr in iface.ips - if addr.is_IPv4 - and addr.network_prefix != 32 # Host only netmask 255.255.255.255 - } - ) - - -def new_socket(): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - # SO_REUSEADDR should be equivalent to SO_REUSEPORT for - # multicast UDP sockets (p 731, "TCP/IP Illustrated, - # Volume 2"), but some BSD-derived systems require - # SO_REUSEPORT to be specified explicitly. Also, not all - # versions of Python have SO_REUSEPORT available. - # Catch OSError and socket.error for kernel versions <3.9 because lacking - # SO_REUSEPORT support. - try: - reuseport = socket.SO_REUSEPORT - except AttributeError: - pass - else: - try: - s.setsockopt(socket.SOL_SOCKET, reuseport, 1) - except OSError as err: - # OSError on python 3, socket.error on python 2 - if err.errno != errno.ENOPROTOOPT: - raise - - # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and - # IP_MULTICAST_LOOP socket options as an unsigned char. - ttl = struct.pack(b"B", 255) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - loop = struct.pack(b"B", 1) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) - - s.bind(("", _MDNS_PORT)) - return s - - -class Zeroconf(QuietLogger): - def __init__(self): - # hook for threads - self._GLOBAL_DONE = False - - self._listen_socket = new_socket() - interfaces = get_all_addresses() - - self._respond_sockets = [] - - for i in interfaces: - try: - _value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i) - self._listen_socket.setsockopt( - socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value - ) - except OSError as e: - _errno = e.args[0] - if _errno == errno.EADDRINUSE: - log.info( - "Address in use when adding %s to multicast group, " - "it is expected to happen on some systems", - i, - ) - elif _errno == errno.EADDRNOTAVAIL: - log.info( - "Address not available when adding %s to multicast " - "group, it is expected to happen on some systems", - i, - ) - continue - elif _errno == errno.EINVAL: - log.info( - "Interface of %s does not support multicast, " - "it is expected in WSL", - i, - ) - continue - - else: - raise - - respond_socket = new_socket() - respond_socket.setsockopt( - socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(i) - ) - - self._respond_sockets.append(respond_socket) - - self.listeners = [] - - self.condition = threading.Condition() - - self.engine = Engine(self) - self.listener = Listener(self) - self.engine.add_reader(self.listener, self._listen_socket) - - @property - def done(self): - return self._GLOBAL_DONE - - def wait(self, timeout): - """Calling thread waits for a given number of milliseconds or - until notified.""" - with self.condition: - self.condition.wait(timeout) - - def notify_all(self): - """Notifies all waiting threads""" - with self.condition: - self.condition.notify_all() - - def resolve_host(self, host, timeout=3.0): +class EsphomeZeroconf(Zeroconf): + def resolve_host(self, host: str, timeout=3.0): info = HostResolver(host) if info.request(self, timeout): return socket.inet_ntoa(info.address) return None - - def add_listener(self, listener): - self.listeners.append(listener) - self.notify_all() - - def remove_listener(self, listener): - """Removes a listener.""" - try: - self.listeners.remove(listener) - self.notify_all() - except Exception as e: # pylint: disable=broad-except - log.exception("Unknown error, possibly benign: %r", e) - - def update_record(self, now, rec): - """Used to notify listeners of new information that has updated - a record.""" - for listener in self.listeners: - listener.update_record(self, now, rec) - self.notify_all() - - def handle_response(self, msg): - """Deal with incoming response packets. All answers - are held in the cache, and listeners are notified.""" - now = time.time() - for record in msg.answers: - self.update_record(now, record) - - def send(self, out): - """Sends an outgoing packet.""" - packet = out.packet() - log.debug("Sending %r (%d bytes) as %r...", out, len(packet), packet) - for s in self._respond_sockets: - if self._GLOBAL_DONE: - return - try: - bytes_sent = s.sendto(packet, 0, (_MDNS_ADDR, _MDNS_PORT)) - except Exception: # pylint: disable=broad-except - # on send errors, log the exception and keep going - self.log_exception_warning() - else: - if bytes_sent != len(packet): - self.log_warning_once( - "!!! sent %d out of %d bytes to %r" - % (bytes_sent, len(packet), s) - ) - - def close(self): - """Ends the background threads, and prevent this instance from - servicing further queries.""" - if not self._GLOBAL_DONE: - self._GLOBAL_DONE = True - # shutdown recv socket and thread - self.engine.del_reader(self._listen_socket) - self._listen_socket.close() - self.engine.join() - - # shutdown the rest - self.notify_all() - for s in self._respond_sockets: - s.close() diff --git a/platformio.ini b/platformio.ini index c280c54a21..f4dea3fcb9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,6 +36,8 @@ lib_deps = 6306@1.0.3 ; HM3301 glmnet/Dsmr@0.3 ; used by dsmr rweather/Crypto@0.2.0 ; used by dsmr + esphome/noise-c@0.1.1 ; used by api + dudanov/MideaUART@1.1.0 ; used by midea build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE diff --git a/requirements.txt b/requirements.txt index b4d557f06e..daaf86e641 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==3.1 click==7.1.2 -esphome-dashboard==20210728.0 +esphome-dashboard==20210908.0 diff --git a/requirements_test.txt b/requirements_test.txt index 684582bd4c..59085b33e2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,11 +1,11 @@ -pylint==2.9.6 +pylint==2.10.2 flake8==3.9.2 -black==21.7b0 +black==21.8b0 pexpect==4.8.0 pre-commit # Unit tests -pytest==6.2.4 +pytest==6.2.5 pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-asyncio==0.15.1 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 6983090fd9..7ccdc5a24e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -778,9 +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"#ifdef HAS_PROTO_MESSAGE_DUMP\n" cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - cout += f'#endif\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" @@ -794,9 +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"#ifdef HAS_PROTO_MESSAGE_DUMP\n" case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' - case += f'#endif\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_jsonschema.py b/script/build_jsonschema.py index 1ab0ffa015..7a3257411c 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -25,6 +25,11 @@ JSC_DESCRIPTION = "description" JSC_ONEOF = "oneOf" JSC_PROPERTIES = "properties" JSC_REF = "$ref" + +# this should be required, but YAML Language server completion does not work properly if required are specified. +# still needed for other features / checks +JSC_REQUIRED = "required_" + SIMPLE_AUTOMATION = "simple_automation" schema_names = {} @@ -295,9 +300,17 @@ def get_automation_schema(name, vschema): # * an object with automation's schema and a then key # with again a single action or an array of actions + if len(extra_jschema[JSC_PROPERTIES]) == 0: + return get_ref(SIMPLE_AUTOMATION) + extra_jschema[JSC_PROPERTIES]["then"] = add_definition_array_or_single_object( get_ref(JSC_ACTION) ) + # if there is a required element in extra_jschema then this automation does not support + # directly a list of actions + if JSC_REQUIRED in extra_jschema: + return create_ref(name, extra_vschema, extra_jschema) + jschema = add_definition_array_or_single_object(get_ref(JSC_ACTION)) jschema[JSC_ANYOF].append(extra_jschema) @@ -370,9 +383,14 @@ def get_entry(parent_key, vschema): # everything else just accept string and let ESPHome validate try: from esphome.core import ID + from esphome.automation import Trigger, Automation v = vschema(None) if isinstance(v, ID): + if v.type.base != "script::Script" and ( + v.type.inherits_from(Trigger) or v.type == Automation + ): + return None entry = {"type": "string", "id_type": v.type.base} elif isinstance(v, str): entry = {"type": "string"} @@ -494,9 +512,11 @@ def convert_schema(path, vschema, un_extend=True): output = {} if str(vschema) in ejs.hidden_schemas: - # this can get another think twist. When adding this I've already figured out - # interval and script in other way - if path not in ["interval", "script"]: + if ejs.hidden_schemas[str(vschema)] == "automation": + vschema = vschema(ejs.jschema_extractor) + jschema = get_jschema(path, vschema, True) + return add_definition_array_or_single_object(jschema) + else: vschema = vschema(ejs.jschema_extractor) if un_extend: @@ -515,9 +535,8 @@ def convert_schema(path, vschema, un_extend=True): return rhs # merge - if JSC_ALLOF in lhs and JSC_ALLOF in rhs: - output = lhs[JSC_ALLOF] + output = lhs for k in rhs[JSC_ALLOF]: merge(output[JSC_ALLOF], k) elif JSC_ALLOF in lhs: @@ -574,6 +593,7 @@ def convert_schema(path, vschema, un_extend=True): return output props = output[JSC_PROPERTIES] = {} + required = [] output["type"] = ["object", "null"] if DUMP_COMMENTS: @@ -616,13 +636,21 @@ def convert_schema(path, vschema, un_extend=True): if prop: # Deprecated (cv.Invalid) properties not added props[str(k)] = prop # TODO: see required, sometimes completions doesn't show up because of this... - # if isinstance(k, cv.Required): - # required.append(str(k)) + if isinstance(k, cv.Required): + required.append(str(k)) try: if str(k.default) != "...": - prop["default"] = k.default() + default_value = k.default() + # Yaml validator fails if `"default": null` ends up in the json schema + if default_value is not None: + if prop["type"] == "string": + default_value = str(default_value) + prop["default"] = default_value except: pass + + if len(required) > 0: + output[JSC_REQUIRED] = required return output @@ -648,6 +676,7 @@ def add_pin_registry(): internal = definitions[schema_name] definitions[schema_name]["additionalItems"] = False definitions[f"PIN.{mode}_INTERNAL"] = internal + internal[JSC_PROPERTIES]["number"] = {"type": ["number", "string"]} schemas = [get_ref(f"PIN.{mode}_INTERNAL")] schemas[0]["required"] = ["number"] # accept string and object, for internal shorthand pin IO: diff --git a/script/ci-custom.py b/script/ci-custom.py index 5dad3e2445..cdc450a96b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -261,7 +261,7 @@ def highlight(s): @lint_re_check( r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL, include=cpp_include, - exclude=["esphome/core/log.h"], + exclude=["esphome/core/log.h", "esphome/components/socket/headers.h"], ) def lint_no_defines(fname, match): s = highlight( @@ -493,7 +493,10 @@ def lint_relative_py_import(fname): "esphome/components/*.h", "esphome/components/*.cpp", "esphome/components/*.tcc", - ] + ], + exclude=[ + "esphome/components/socket/headers.h", + ], ) def lint_namespace(fname, content): expected_name = re.match( diff --git a/setup.py b/setup.py index 44a5965887..967eadd70f 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME) GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH) -DOWNLOAD_URL = "{}/archive/v{}.zip".format(GITHUB_URL, const.__version__) +DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, const.__version__) here = os.path.abspath(os.path.dirname(__file__)) diff --git a/tests/test1.yaml b/tests/test1.yaml index bcf5f932a8..cd4179f394 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -129,6 +129,8 @@ mqtt: - mqtt.connected: - light.is_on: kitchen - light.is_off: kitchen + - fan.is_on: fan_speed + - fan.is_off: fan_speed then: - lambda: |- int data = x["my_data"]; @@ -1606,29 +1608,84 @@ climate: name: Toshiba Climate - platform: hitachi_ac344 name: Hitachi Climate - - platform: midea_ac + - platform: midea + id: midea_unit + uart_id: uart0 + name: Midea Climate + transmitter_id: + period: 1s + num_attempts: 5 + timeout: 2s + beeper: false + autoconf: true visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: 'Electrolux EACS' - beeper: true + min_temperature: 17 °C + max_temperature: 30 °C + temperature_step: 0.5 °C + supported_modes: + - FAN_ONLY + - HEAT_COOL + - COOL + - HEAT + - DRY + custom_fan_modes: + - SILENT + - TURBO + supported_presets: + - ECO + - BOOST + - SLEEP + custom_presets: + - FREEZE_PROTECTION + supported_swing_modes: + - VERTICAL + - HORIZONTAL + - BOTH outdoor_temperature: - name: 'Temp' + name: "Temp" power_usage: - name: 'Power' + name: "Power" humidity_setpoint: - name: 'Hum' + name: "Humidity" - platform: anova name: Anova cooker ble_client_id: ble_blah unit_of_measurement: c -midea_dongle: - uart_id: uart0 - strength_icon: true +script: + - id: climate_custom + then: + - climate.control: + id: midea_unit + custom_preset: FREEZE_PROTECTION + custom_fan_mode: SILENT + - id: climate_preset + then: + - climate.control: + id: midea_unit + preset: SLEEP switch: + - platform: template + name: MIDEA_AC_TOGGLE_LIGHT + turn_on_action: + midea_ac.display_toggle: + - platform: template + name: MIDEA_AC_SWING_STEP + turn_on_action: + midea_ac.swing_step: + - platform: template + name: MIDEA_AC_BEEPER_CONTROL + optimistic: true + turn_on_action: + midea_ac.beeper_on: + turn_off_action: + midea_ac.beeper_off: + - platform: template + name: MIDEA_RAW + turn_on_action: + remote_transmitter.transmit_midea: + code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] - platform: gpio name: 'MCP23S08 Pin #0' pin: @@ -1860,6 +1917,7 @@ switch: inverted: False - platform: template id: ble1_status + optimistic: true fan: - platform: binary @@ -1868,6 +1926,7 @@ fan: oscillation_output: gpio_19 direction_output: gpio_26 - platform: speed + id: fan_speed output: pca_6 speed_count: 10 name: 'Living Room Fan 2' @@ -1877,6 +1936,9 @@ fan: oscillation_command_topic: oscillation/command/topic speed_state_topic: speed/state/topic speed_command_topic: speed/command/topic + on_speed_set: + then: + - logger.log: "Fan speed was changed!" interval: - interval: 10s @@ -2038,6 +2100,14 @@ display: backlight_pin: GPIO4 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7920 + width: 128 + height: 64 + cs_pin: + number: GPIO23 + inverted: true + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 model: 'INITR_BLACKTAB' cs_pin: GPIO5 diff --git a/tests/test2.yaml b/tests/test2.yaml index 6807278c0d..54932953d5 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -246,6 +246,24 @@ sensor: id: freezer_temp_source reference_voltage: 3.19 number: 0 + - platform: airthings_wave_plus + ble_client_id: airthings01 + update_interval: 5min + temperature: + name: "Wave Plus Temperature" + radon: + name: "Wave Plus Radon" + radon_long_term: + name: "Wave Plus Radon Long Term" + pressure: + name: "Wave Plus Pressure" + humidity: + name: "Wave Plus Humidity" + co2: + name: "Wave Plus CO2" + tvoc: + name: "Wave Plus VOC" + time: - platform: homeassistant on_time: @@ -276,6 +294,11 @@ binary_sensor: - platform: ble_presence service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' name: 'BLE Test Service 128 Presence' + - platform: ble_presence + ibeacon_uuid: '11223344-5566-7788-99aa-bbccddeeff00' + ibeacon_major: 100 + ibeacon_minor: 1 + name: 'BLE Test iBeacon Presence' - platform: esp32_touch name: 'ESP32 Touch Pad GPIO27' pin: GPIO27 @@ -334,6 +357,12 @@ esp32_ble_tracker: - lambda: !lambda |- ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); +ble_client: + - mac_address: 01:02:03:04:05:06 + id: airthings01 + +airthings_ble: + #esp32_ble_beacon: # type: iBeacon # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' @@ -431,3 +460,4 @@ interval: - logger.log: 'Interval Run' display: + diff --git a/tests/test3.yaml b/tests/test3.yaml index e35c1e611c..5602481c36 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -22,6 +22,8 @@ api: port: 8000 password: 'pwd' reboot_timeout: 0min + encryption: + key: 'bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=' services: - service: hello_world variables: @@ -748,17 +750,6 @@ script: - id: my_script then: - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: climate_custom - then: - - climate.control: - id: midea_ac_unit - custom_preset: FREEZE_PROTECTION - custom_fan_mode: SILENT - - id: climate_preset - then: - - climate.control: - id: midea_ac_unit - preset: SLEEP sm2135: data_pin: GPIO12 @@ -949,32 +940,6 @@ climate: kp: 0.0 ki: 0.0 kd: 0.0 - - platform: midea_ac - id: midea_ac_unit - visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: "Electrolux EACS" - beeper: true - custom_fan_modes: - - SILENT - - TURBO - preset_eco: true - preset_sleep: true - preset_boost: true - custom_presets: - - FREEZE_PROTECTION - outdoor_temperature: - name: "Temp" - power_usage: - name: "Power" - humidity_setpoint: - name: "Hum" - -midea_dongle: - uart_id: uart1 - strength_icon: true cover: - platform: endstop diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 9e4ad3d79d..37b4d6db57 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -473,7 +473,11 @@ 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__", + 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_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 3e317589a9..ae7a61e01f 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -38,7 +38,7 @@ async def test_register_component(monkeypatch): actual = await ch.register_component(var, {}) assert actual is var - add_mock.assert_called_once() + assert add_mock.call_count == 2 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] @@ -77,6 +77,6 @@ async def test_register_component__with_setup_priority(monkeypatch): assert actual is var add_mock.assert_called() - assert add_mock.call_count == 3 + assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == []