From e6834f25ed7355f6f9c27155e8da761da433e06f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 11:08:46 -1000 Subject: [PATCH 01/31] Fix bulk and single Bluetooth parser coexistence (#5073) --- .../bluetooth_proxy/bluetooth_connection.cpp | 4 ++ .../bluetooth_proxy/bluetooth_connection.h | 1 + .../bluetooth_proxy/bluetooth_proxy.cpp | 8 ++++ .../bluetooth_proxy/bluetooth_proxy.h | 1 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 42 +++++++++++++++---- .../esp32_ble_tracker/esp32_ble_tracker.h | 17 +++++--- 6 files changed, 60 insertions(+), 13 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 26304325c1..97a25262cb 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -275,6 +275,10 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl return ESP_OK; } +esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() { + return this->proxy_->get_advertisement_parser_type(); +} + } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 8b13f4d1c2..e6ab3cbccc 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -14,6 +14,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp_err_t read_characteristic(uint16_t handle); esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index b633fe2430..f188439d0e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -198,6 +198,12 @@ void BluetoothProxy::loop() { } } +esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { + if (this->raw_advertisements_) + return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; + return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS; +} + BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { for (auto *connection : this->connections_) { if (connection->get_address() == address) @@ -435,6 +441,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection } this->api_connection_ = api_connection; this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; + this->parent_->recalculate_advertisement_parser_types(); } void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { @@ -444,6 +451,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti } this->api_connection_ = nullptr; this->raw_advertisements_ = false; + this->parent_->recalculate_advertisement_parser_types(); } void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 97b6396b55..35a37f934a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -51,6 +51,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; void dump_config() override; void loop() override; + esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { this->connections_.push_back(connection); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0f6c4117d2..1569ea0dd5 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -107,16 +107,16 @@ void ESP32BLETracker::loop() { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); } - bool bulk_parsed = false; - - for (auto *listener : this->listeners_) { - bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); - } - for (auto *client : this->clients_) { - bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + if (this->raw_advertisements_) { + for (auto *listener : this->listeners_) { + listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + for (auto *client : this->clients_) { + client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } } - if (!bulk_parsed) { + if (this->parse_advertisements_) { for (size_t i = 0; i < index; i++) { ESPBTDevice device; device.parse_scan_rst(this->scan_result_buffer_[i]); @@ -284,6 +284,32 @@ void ESP32BLETracker::end_of_scan_() { void ESP32BLETracker::register_client(ESPBTClient *client) { client->app_id = ++this->app_id_; this->clients_.push_back(client); + this->recalculate_advertisement_parser_types(); +} + +void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { + listener->set_parent(this); + this->listeners_.push_back(listener); + this->recalculate_advertisement_parser_types(); +} + +void ESP32BLETracker::recalculate_advertisement_parser_types() { + this->raw_advertisements_ = false; + this->parse_advertisements_ = false; + for (auto *listener : this->listeners_) { + if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { + this->parse_advertisements_ = true; + } else { + this->raw_advertisements_ = true; + } + } + for (auto *client : this->clients_) { + if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { + this->parse_advertisements_ = true; + } else { + this->raw_advertisements_ = true; + } + } } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 43e88fbf2b..6efdded3ff 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -27,6 +27,11 @@ using namespace esp32_ble; using adv_data_t = std::vector; +enum AdvertisementParserType { + PARSED_ADVERTISEMENTS, + RAW_ADVERTISEMENTS, +}; + struct ServiceData { ESPBTUUID uuid; adv_data_t data; @@ -116,6 +121,9 @@ class ESPBTDeviceListener { virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { return false; }; + virtual AdvertisementParserType get_advertisement_parser_type() { + return AdvertisementParserType::PARSED_ADVERTISEMENTS; + }; void set_parent(ESP32BLETracker *parent) { parent_ = parent; } protected: @@ -184,12 +192,9 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv void loop() override; - void register_listener(ESPBTDeviceListener *listener) { - listener->set_parent(this); - this->listeners_.push_back(listener); - } - + void register_listener(ESPBTDeviceListener *listener); void register_client(ESPBTClient *client); + void recalculate_advertisement_parser_types(); void print_bt_device_info(const ESPBTDevice &device); @@ -231,6 +236,8 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv bool scan_continuous_; bool scan_active_; bool scanner_idle_; + bool raw_advertisements_{false}; + bool parse_advertisements_{false}; SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; From 8739552c0b24005ae990067cd854ecbf63fcb71c Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sun, 9 Jul 2023 17:55:02 -0400 Subject: [PATCH 02/31] binary_sensor: Validate max_length for on_click/on_double_click (#5068) --- esphome/components/binary_sensor/__init__.py | 58 +++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 41b4c5a0d7..95e35a45f2 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -359,6 +359,18 @@ def validate_multi_click_timing(value): validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +def validate_click_timing(value): + for v in value: + min_length = v.get(CONF_MIN_LENGTH) + max_length = v.get(CONF_MAX_LENGTH) + if max_length < min_length: + raise cv.Invalid( + f"Max length ({max_length}) must be larger than min length ({min_length})." + ) + + return value + + BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(BinarySensor), @@ -378,27 +390,33 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), } ), - cv.Optional(CONF_ON_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), - cv.Optional( - CONF_MIN_LENGTH, default="50ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_MAX_LENGTH, default="350ms" - ): cv.positive_time_period_milliseconds, - } + cv.Optional(CONF_ON_CLICK): cv.All( + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + validate_click_timing, ), - cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), - cv.Optional( - CONF_MIN_LENGTH, default="50ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_MAX_LENGTH, default="350ms" - ): cv.positive_time_period_milliseconds, - } + cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All( + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + validate_click_timing, ), cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation( { From 8bf8892ab34cc3f5688ab006312652c0161c847a Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 10 Jul 2023 00:02:42 +0200 Subject: [PATCH 03/31] [Ethernet] ksz8081rna support (#4739) Co-authored-by: Your Name --- esphome/components/ethernet/__init__.py | 1 + .../ethernet/ethernet_component.cpp | 44 ++++++++++++++++++- .../components/ethernet/ethernet_component.h | 3 ++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index bedc0a4c30..6f0f3741dd 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -35,6 +35,7 @@ ETHERNET_TYPES = { "IP101": EthernetType.ETHERNET_TYPE_IP101, "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, + "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, } emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fc1068f2a8..3b5804abdd 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -84,7 +84,8 @@ void EthernetComponent::setup() { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; } - case ETHERNET_TYPE_KSZ8081: { + case ETHERNET_TYPE_KSZ8081: + case ETHERNET_TYPE_KSZ8081RNA: { #if ESP_IDF_VERSION_MAJOR >= 5 this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); #else @@ -102,6 +103,12 @@ void EthernetComponent::setup() { this->eth_handle_ = nullptr; err = esp_eth_driver_install(ð_config, &this->eth_handle_); ESPHL_ERROR_CHECK(err, "ETH driver install error"); + + if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { + // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. + this->ksz8081_set_clock_reference_(mac); + } + /* attach Ethernet driver to TCP/IP stack */ err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); ESPHL_ERROR_CHECK(err, "ETH netif attach error"); @@ -184,6 +191,10 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081"; break; + case ETHERNET_TYPE_KSZ8081RNA: + eth_type = "KSZ8081RNA"; + break; + default: eth_type = "Unknown"; break; @@ -385,6 +396,37 @@ bool EthernetComponent::powerdown() { return true; } +void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { +#define KSZ80XX_PC2R_REG_ADDR (0x1F) + + esp_err_t err; + + uint32_t phy_control_2; + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + + /* + * Bit 7 is `RMII Reference Clock Select`. Default is `0`. + * KSZ8081RNA: + * 0 - clock input to XI (Pin 8) is 25 MHz for RMII – 25 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode. + * KSZ8081RND: + * 0 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII – 25 MHz clock mode. + */ + if ((phy_control_2 & (1 << 7)) != (1 << 7)) { + phy_control_2 |= 1 << 7; + err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); + ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + } + +#undef KSZ80XX_PC2R_REG_ADDR +} + } // namespace ethernet } // namespace esphome diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 918e47212f..f6b67f3f82 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -21,6 +21,7 @@ enum EthernetType { ETHERNET_TYPE_IP101, ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, + ETHERNET_TYPE_KSZ8081RNA, }; struct ManualIP { @@ -67,6 +68,8 @@ class EthernetComponent : public Component { void start_connect_(); void dump_connect_params_(); + /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. + void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); std::string use_address_; uint8_t phy_addr_{0}; From 8ca9115dc8efd1274e441aa65d7249f12ba4d2df Mon Sep 17 00:00:00 2001 From: Trevor North Date: Sun, 9 Jul 2023 23:03:54 +0100 Subject: [PATCH 04/31] Improve BME680 BSEC sensor device classes (#4859) --- esphome/components/bme680_bsec/sensor.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index 8d00012150..3bd082481e 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -6,8 +6,10 @@ from esphome.const import ( CONF_HUMIDITY, CONF_PRESSURE, CONF_TEMPERATURE, + DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -17,8 +19,6 @@ from esphome.const import ( UNIT_PERCENT, ICON_GAS_CYLINDER, ICON_GAUGE, - ICON_THERMOMETER, - ICON_WATER_PERCENT, ) from . import ( BME680BSECComponent, @@ -35,7 +35,6 @@ CONF_CO2_EQUIVALENT = "co2_equivalent" CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" UNIT_IAQ = "IAQ" ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" -ICON_TEST_TUBE = "mdi:test-tube" TYPES = [ CONF_TEMPERATURE, @@ -53,7 +52,6 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -62,16 +60,14 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( unit_of_measurement=UNIT_HECTOPASCAL, - icon=ICON_GAUGE, accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - icon=ICON_WATER_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, @@ -97,14 +93,14 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, - icon=ICON_TEST_TUBE, accuracy_decimals=1, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, - icon=ICON_TEST_TUBE, accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), } From c5aacdd68250a0c55136ec49c114aed14b2cad5b Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 10 Jul 2023 01:30:39 +0200 Subject: [PATCH 05/31] Update RP2040 Aruino framwork and platform to latest (#5025) --- esphome/components/rp2040/__init__.py | 8 ++++---- platformio.ini | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 030d586626..dafafc531c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -62,19 +62,19 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 6, 4) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 3, 0) # The platformio/raspberrypi version to use for arduino frameworks # - https://github.com/platformio/platform-raspberrypi/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi -ARDUINO_PLATFORM_VERSION = cv.Version(1, 7, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(1, 9, 0) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 6, 4), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(2, 6, 4), None), + "dev": (cv.Version(3, 3, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(3, 3, 0), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/platformio.ini b/platformio.ini index 868880e1d7..b970ef7a69 100644 --- a/platformio.ini +++ b/platformio.ini @@ -157,7 +157,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.3.0/rp2040-3.3.0.zip framework = arduino lib_deps = From ddde1ee31ef96476c7a7d1c8b8fe306aa4d2e503 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 10 Jul 2023 01:34:43 +0200 Subject: [PATCH 06/31] Allow pillow versions over 10 (#5071) --- esphome/components/font/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 7a314bb032..29150afe3f 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path import hashlib import os import re +from packaging import version import requests @@ -69,7 +70,7 @@ def validate_pillow_installed(value): "(pip install pillow)" ) from err - if PIL.__version__[0] < "4": + if version.parse(PIL.__version__) < version.parse("4.0.0"): raise cv.Invalid( "Please update your pillow installation to at least 4.0.x. " "(pip install -U pillow)" From 98fd092053bef74a6349cc92f70d5602217efeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Tue, 11 Jul 2023 06:38:28 +0900 Subject: [PATCH 07/31] display: rename `DisplayBufferRef` to `DisplayRef` (#5002) --- esphome/components/addressable_light/display.py | 2 +- esphome/components/display/__init__.py | 5 +++-- esphome/components/display/display.h | 9 --------- esphome/components/ili9xxx/display.py | 2 +- esphome/components/inkplate6/display.py | 2 +- esphome/components/pcd8544/display.py | 2 +- esphome/components/ssd1306_base/__init__.py | 2 +- esphome/components/ssd1322_base/__init__.py | 2 +- esphome/components/ssd1325_base/__init__.py | 2 +- esphome/components/ssd1327_base/__init__.py | 2 +- esphome/components/ssd1331_base/__init__.py | 2 +- esphome/components/ssd1351_base/__init__.py | 2 +- esphome/components/st7735/display.py | 2 +- esphome/components/st7789v/display.py | 2 +- esphome/components/waveshare_epaper/display.py | 2 +- 15 files changed, 16 insertions(+), 24 deletions(-) diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py index 0684bf8dfc..5fdd84ac2d 100644 --- a/esphome/components/addressable_light/display.py +++ b/esphome/components/addressable_light/display.py @@ -58,6 +58,6 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 0d403f99f0..b7a8508fc8 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -18,10 +18,11 @@ from esphome.core import coroutine_with_priority IS_PLATFORM_COMPONENT = True display_ns = cg.esphome_ns.namespace("display") +Display = display_ns.class_("Display") DisplayBuffer = display_ns.class_("DisplayBuffer") DisplayPage = display_ns.class_("DisplayPage") DisplayPagePtr = DisplayPage.operator("ptr") -DisplayBufferRef = DisplayBuffer.operator("ref") +DisplayRef = Display.operator("ref") DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action) DisplayPageShowNextAction = display_ns.class_( "DisplayPageShowNextAction", automation.Action @@ -96,7 +97,7 @@ async def setup_display_core_(var, config): pages = [] for conf in config[CONF_PAGES]: lambda_ = await cg.process_lambda( - conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void + conf[CONF_LAMBDA], [(DisplayRef, "it")], return_type=cg.void ) page = cg.new_Pvariable(conf[CONF_ID], lambda_) pages.append(page) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 2f6fb563cc..08d8c70e0d 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -133,12 +133,10 @@ enum DisplayRotation { }; class Display; -class DisplayBuffer; class DisplayPage; class DisplayOnPageChangeTrigger; using display_writer_t = std::function; -using display_buffer_writer_t = std::function; #define LOG_DISPLAY(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -411,10 +409,6 @@ class Display { /// Internal method to set the display writer lambda. void set_writer(display_writer_t &&writer); - void set_writer(const display_buffer_writer_t &writer) { - // Temporary mapping to be removed once all lambdas are changed to use `display.DisplayRef` - this->set_writer([writer](Display &display) { return writer((display::DisplayBuffer &) display); }); - } void show_page(DisplayPage *page); void show_next_page(); @@ -499,9 +493,6 @@ class Display { class DisplayPage { public: DisplayPage(display_writer_t writer); - // Temporary mapping to be removed once all lambdas are changed to use `display.DisplayRef` - DisplayPage(const display_buffer_writer_t &writer) - : DisplayPage([writer](Display &display) { return writer((display::DisplayBuffer &) display); }) {} void show(); void show_next(); void show_prev(); diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 6021da5eeb..0435460b6a 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -121,7 +121,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a17f37c920..7534731175 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -115,7 +115,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index d0184a58d3..b4c8f432cf 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -52,6 +52,6 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 48143b9e1a..f4abd845c8 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -96,6 +96,6 @@ async def setup_ssd1306(var, config): cg.add(var.init_invert(config[CONF_INVERT])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py index 434caf4e35..97fb0d2a74 100644 --- a/esphome/components/ssd1322_base/__init__.py +++ b/esphome/components/ssd1322_base/__init__.py @@ -46,6 +46,6 @@ async def setup_ssd1322(var, config): cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 68be287d2a..1a6f7fb519 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -50,6 +50,6 @@ async def setup_ssd1325(var, config): cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py index eada66a6e3..af2eb3489d 100644 --- a/esphome/components/ssd1327_base/__init__.py +++ b/esphome/components/ssd1327_base/__init__.py @@ -37,6 +37,6 @@ async def setup_ssd1327(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py index 067f55a252..169c0eed1a 100644 --- a/esphome/components/ssd1331_base/__init__.py +++ b/esphome/components/ssd1331_base/__init__.py @@ -28,6 +28,6 @@ async def setup_ssd1331(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index 555d6c5e2e..2988dd4bf3 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -38,6 +38,6 @@ async def setup_ssd1351(var, config): cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index ae31f604a5..652d31662d 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -77,7 +77,7 @@ async def setup_st7735(var, config): cg.add(var.set_reset_pin(reset)) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a81101f2d1..16c1e790bd 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -121,7 +121,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 11deab5310..6113fe943c 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -153,7 +153,7 @@ async def to_code(config): if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) if CONF_RESET_PIN in config: From a39181592155a15ae908fff657f7e26e0cdba935 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Tue, 11 Jul 2023 00:24:18 -0400 Subject: [PATCH 08/31] Add Zio Ultrasonic Distance Sensor Component (#5059) --- CODEOWNERS | 1 + esphome/components/zio_ultrasonic/__init__.py | 0 esphome/components/zio_ultrasonic/sensor.py | 34 +++++++++++++++++++ .../zio_ultrasonic/zio_ultrasonic.cpp | 31 +++++++++++++++++ .../zio_ultrasonic/zio_ultrasonic.h | 22 ++++++++++++ tests/test1.yaml | 4 +++ 6 files changed, 92 insertions(+) create mode 100644 esphome/components/zio_ultrasonic/__init__.py create mode 100644 esphome/components/zio_ultrasonic/sensor.py create mode 100644 esphome/components/zio_ultrasonic/zio_ultrasonic.cpp create mode 100644 esphome/components/zio_ultrasonic/zio_ultrasonic.h diff --git a/CODEOWNERS b/CODEOWNERS index 94813f73dd..ca2c259624 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -321,3 +321,4 @@ esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/* @nielsnl68 @numo68 +esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/zio_ultrasonic/__init__.py b/esphome/components/zio_ultrasonic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zio_ultrasonic/sensor.py b/esphome/components/zio_ultrasonic/sensor.py new file mode 100644 index 0000000000..c5eed14e64 --- /dev/null +++ b/esphome/components/zio_ultrasonic/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +zio_ultrasonic_ns = cg.esphome_ns.namespace("zio_ultrasonic") + +ZioUltrasonicComponent = zio_ultrasonic_ns.class_( + "ZioUltrasonicComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + ZioUltrasonicComponent, + unit_of_measurement="mm", + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x00)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp new file mode 100644 index 0000000000..565bbe9b4f --- /dev/null +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp @@ -0,0 +1,31 @@ + +#include "zio_ultrasonic.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace zio_ultrasonic { + +static const char *const TAG = "zio_ultrasonic"; + +void ZioUltrasonicComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Zio Ultrasonic Sensor:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Sensor:", this); +} + +void ZioUltrasonicComponent::update() { + uint16_t distance; + + // Read an unsigned two byte integerfrom register 0x01 which gives distance in mm + if (!this->read_byte_16(0x01, &distance)) { + ESP_LOGE(TAG, "Error reading data from Zio Ultrasonic"); + this->publish_state(NAN); + } else { + this->publish_state(distance); + } +} + +} // namespace zio_ultrasonic +} // namespace esphome diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h new file mode 100644 index 0000000000..84c8d44c65 --- /dev/null +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +static const char *const TAG = "Zio Ultrasonic"; + +namespace esphome { +namespace zio_ultrasonic { + +class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + void update() override; +}; + +} // namespace zio_ultrasonic +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 05ba07d1d8..21379e807c 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1310,6 +1310,10 @@ sensor: fault_count: 1 polarity: active_high function: comparator + - platform: zio_ultrasonic + name: "Distance" + update_interval: 60s + i2c_id: i2c_bus esp32_touch: setup_mode: false From f3cdcc008a01133dcf576ffe6bb712bca49fd05b Mon Sep 17 00:00:00 2001 From: jan-hofmeier <37298146+jan-hofmeier@users.noreply.github.com> Date: Tue, 11 Jul 2023 07:12:43 +0200 Subject: [PATCH 09/31] Add Alpha3 pump component (#3787) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/alpha3/__init__.py | 1 + esphome/components/alpha3/alpha3.cpp | 189 ++++++++++++++++++++++++++ esphome/components/alpha3/alpha3.h | 73 ++++++++++ esphome/components/alpha3/sensor.py | 85 ++++++++++++ esphome/const.py | 4 + tests/test1.yaml | 11 ++ 7 files changed, 364 insertions(+) create mode 100644 esphome/components/alpha3/__init__.py create mode 100644 esphome/components/alpha3/alpha3.cpp create mode 100644 esphome/components/alpha3/alpha3.h create mode 100644 esphome/components/alpha3/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ca2c259624..68a8eb6d18 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/airthings_wave_base/* @jeromelaban @ncareau esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/alarm_control_panel/* @grahambrown11 +esphome/components/alpha3/* @jan-hofmeier esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix diff --git a/esphome/components/alpha3/__init__.py b/esphome/components/alpha3/__init__.py new file mode 100644 index 0000000000..7cd320c80f --- /dev/null +++ b/esphome/components/alpha3/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jan-hofmeier"] diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp new file mode 100644 index 0000000000..17899c31cb --- /dev/null +++ b/esphome/components/alpha3/alpha3.cpp @@ -0,0 +1,189 @@ +#include "alpha3.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include //gives ntohl + +#ifdef USE_ESP32 + +namespace esphome { +namespace alpha3 { + +static const char *const TAG = "alpha3"; + +void Alpha3::dump_config() { + ESP_LOGCONFIG(TAG, "ALPHA3"); + LOG_SENSOR(" ", "Flow", this->flow_sensor_); + LOG_SENSOR(" ", "Head", this->head_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); +} + +void Alpha3::setup() {} + +void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor) { + if (sensor == nullptr) + return; + // we need to handle cases where a value is split over two packets + const int16_t value_length = 4; // 32bit float + // offset inside current response packet + auto rel_offset = value_offset - response_offset; + if (rel_offset <= -value_length) + return; // aready passed the value completly + if (rel_offset >= length) + return; // value not in this packet + + auto start_offset = std::max(0, rel_offset); + auto end_offset = std::min((int16_t) (rel_offset + value_length), length); + auto copy_length = end_offset - start_offset; + auto buffer_offset = std::max(-rel_offset, 0); + std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length); + + if (rel_offset + value_length <= length) { + // we have the whole value + void *buffer = this->buffer_; // to prevent warnings when casting the pointer + *((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian + float fvalue = *((float *) buffer); + sensor->publish_state(fvalue * factor); + } +} + +bool Alpha3::is_current_response_type_(const uint8_t *response_type) { + return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH); +} + +void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { + if (this->response_offset_ >= this->response_length_) { + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + if (length < GENI_RESPONSE_HEADER_LENGTH) { + ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + return; + } + if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + response[0], response[1], response[2], response[3], response[4]); + return; + } + this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum + this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH; + std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH); + } + + auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor, + float factor) { + this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor); + }; + + if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); + extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); + } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F); + } else { + ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1], + this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5], + this->response_type_[6], this->response_type_[7]); + } + this->response_offset_ += length; +} + +void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + this->response_offset_ = 0; + this->response_length_ = 0; + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + break; + } + case ESP_GATTC_CONNECT_EVT: { + if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0) + return; + auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT); + if (ret) { + ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret); + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->node_state = espbt::ClientState::IDLE; + if (this->flow_sensor_ != nullptr) + this->flow_sensor_->publish_state(NAN); + if (this->head_sensor_ != nullptr) + this->head_sensor_->publish_state(NAN); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(NAN); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->voltage_sensor_->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); + if (chr == nullptr) { + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + break; + } + auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), + chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + this->geni_handle_ = chr->handle; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle == this->geni_handle_) { + this->handle_geni_response_(param->notify.value, param->notify.value_len); + } + break; + } + default: + break; + } +} + +void Alpha3::send_request_(uint8_t *request, size_t len) { + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, + request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); +} + +void Alpha3::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + return; + } + + if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) { + uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31}; + this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head)); + delay(25); // need to wait between requests + } + if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr || + this->voltage_sensor_ != nullptr) { + uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205}; + this->send_request_(geni_request_power, sizeof(geni_request_power)); + delay(25); // need to wait between requests + } +} +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h new file mode 100644 index 0000000000..325c70a538 --- /dev/null +++ b/esphome/components/alpha3/alpha3.h @@ -0,0 +1,73 @@ +#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" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace alpha3 { + +namespace espbt = esphome::esp32_ble_tracker; + +static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d); +static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = + espbt::ESPBTUUID::from_raw({static_cast(0xa9), 0x7b, static_cast(0xb8), static_cast(0x85), 0x0, + 0x1a, 0x28, static_cast(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast(0xd1), + static_cast(0xff), static_cast(0x9c), static_cast(0x85)}); +static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13; +static const size_t GENI_RESPONSE_TYPE_LENGTH = 8; + +static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24}; +static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0; +static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4; + +static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37}; +static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0; +static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4; +static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8; +static const int16_t GENI_RESPONSE_POWER_OFFSET = 12; +static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure +static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20; + +class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } + void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } + void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } + void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; } + void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; } + void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; } + + protected: + sensor::Sensor *flow_sensor_{nullptr}; + sensor::Sensor *head_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + uint16_t geni_handle_; + int16_t response_length_; + int16_t response_offset_; + uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH]; + uint8_t buffer_[4]; + void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor); + void handle_geni_response_(const uint8_t *response, uint16_t length); + void send_request_(uint8_t *request, size_t len); + bool is_current_response_type_(const uint8_t *response_type); +}; +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/sensor.py b/esphome/components/alpha3/sensor.py new file mode 100644 index 0000000000..ba4ca16a5a --- /dev/null +++ b/esphome/components/alpha3/sensor.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client +from esphome.const import ( + CONF_ID, + CONF_CURRENT, + CONF_FLOW, + CONF_HEAD, + CONF_POWER, + CONF_SPEED, + CONF_VOLTAGE, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, + UNIT_METER, + UNIT_CUBIC_METER_PER_HOUR, + UNIT_REVOLUTIONS_PER_MINUTE, +) + +alpha3_ns = cg.esphome_ns.namespace("alpha3") +Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Alpha3), + cv.Optional(CONF_FLOW): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR, + accuracy_decimals=2, + ), + cv.Optional(CONF_HEAD): sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + accuracy_decimals=2, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=2, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("15s")) +) + + +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_FLOW in config: + sens = await sensor.new_sensor(config[CONF_FLOW]) + cg.add(var.set_flow_sensor(sens)) + + if CONF_HEAD in config: + sens = await sensor.new_sensor(config[CONF_HEAD]) + cg.add(var.set_head_sensor(sens)) + + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) + + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_SPEED in config: + sens = await sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(sens)) diff --git a/esphome/const.py b/esphome/const.py index 3145255e20..3442c392c7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -262,6 +262,7 @@ CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" +CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" @@ -286,6 +287,7 @@ CONF_GPIO = "gpio" CONF_GREEN = "green" CONF_GROUP = "group" CONF_HARDWARE_UART = "hardware_uart" +CONF_HEAD = "head" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" CONF_HEAT_DEADBAND = "heat_deadband" @@ -891,6 +893,7 @@ UNIT_CENTIMETER = "cm" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" +UNIT_CUBIC_METER_PER_HOUR = "m³/h" UNIT_DECIBEL = "dB" UNIT_DECIBEL_MILLIWATT = "dBm" UNIT_DEGREE_PER_SECOND = "°/s" @@ -928,6 +931,7 @@ UNIT_PERCENT = "%" UNIT_PH = "pH" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" +UNIT_REVOLUTIONS_PER_MINUTE = "RPM" UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" diff --git a/tests/test1.yaml b/tests/test1.yaml index 21379e807c..9d279b5e40 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -325,6 +325,7 @@ ble_client: accept: True - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client + bedjet: - ble_client_id: my_bedjet_ble_client id: my_bedjet_client @@ -1269,6 +1270,16 @@ sensor: pressure: name: "MPL3115A2 Pressure" update_interval: 10s + - platform: alpha3 + ble_client_id: ble_foo + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" - platform: ld2410 moving_distance: name: "Moving distance (cm)" From 74139985c9c6c5ede69c7c629b48f0526284bc0f Mon Sep 17 00:00:00 2001 From: KoenBreeman <121864590+KoenBreeman@users.noreply.github.com> Date: Tue, 11 Jul 2023 23:19:28 +0200 Subject: [PATCH 10/31] RTC implementation of pcf8563 (#4998) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/pcf8563/__init__.py | 0 esphome/components/pcf8563/pcf8563.cpp | 109 ++++++++++++++++++++++ esphome/components/pcf8563/pcf8563.h | 124 +++++++++++++++++++++++++ esphome/components/pcf8563/time.py | 62 +++++++++++++ tests/test5.yaml | 1 + 6 files changed, 297 insertions(+) create mode 100644 esphome/components/pcf8563/__init__.py create mode 100644 esphome/components/pcf8563/pcf8563.cpp create mode 100644 esphome/components/pcf8563/pcf8563.h create mode 100644 esphome/components/pcf8563/time.py diff --git a/CODEOWNERS b/CODEOWNERS index 68a8eb6d18..82612fbd4a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -201,6 +201,7 @@ esphome/components/output/* @esphome/core esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @hwstar esphome/components/pcf85063/* @brogon +esphome/components/pcf8563/* @KoenBreeman esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie diff --git a/esphome/components/pcf8563/__init__.py b/esphome/components/pcf8563/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp new file mode 100644 index 0000000000..f2a82735c5 --- /dev/null +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -0,0 +1,109 @@ +#include "pcf8563.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://nl.mouser.com/datasheet/2/302/PCF8563-1127619.pdf + +namespace esphome { +namespace pcf8563 { + +static const char *const TAG = "PCF8563"; + +void PCF8563Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCF8563..."); + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void PCF8563Component::update() { this->read_time(); } + +void PCF8563Component::dump_config() { + ESP_LOGCONFIG(TAG, "PCF8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCF8563 failed!"); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } + +void PCF8563Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (pcf8563_.reg.stop) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + ESPTime rtc_time{ + .second = uint8_t(pcf8563_.reg.second + 10 * pcf8563_.reg.second_10), + .minute = uint8_t(pcf8563_.reg.minute + 10u * pcf8563_.reg.minute_10), + .hour = uint8_t(pcf8563_.reg.hour + 10u * pcf8563_.reg.hour_10), + .day_of_week = uint8_t(pcf8563_.reg.weekday), + .day_of_month = uint8_t(pcf8563_.reg.day + 10u * pcf8563_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(pcf8563_.reg.month + 10u * pcf8563_.reg.month_10), + .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0, // overwritten by recalc_timestamp_utc(false) + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void PCF8563Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + pcf8563_.reg.year = (now.year - 2000) % 10; + pcf8563_.reg.year_10 = (now.year - 2000) / 10 % 10; + pcf8563_.reg.month = now.month % 10; + pcf8563_.reg.month_10 = now.month / 10; + pcf8563_.reg.day = now.day_of_month % 10; + pcf8563_.reg.day_10 = now.day_of_month / 10; + pcf8563_.reg.weekday = now.day_of_week; + pcf8563_.reg.hour = now.hour % 10; + pcf8563_.reg.hour_10 = now.hour / 10; + pcf8563_.reg.minute = now.minute % 10; + pcf8563_.reg.minute_10 = now.minute / 10; + pcf8563_.reg.second = now.second % 10; + pcf8563_.reg.second_10 = now.second / 10; + pcf8563_.reg.stop = false; + + this->write_rtc_(); +} + +bool PCF8563Component::read_rtc_() { + if (!this->read_bytes(0, this->pcf8563_.raw, sizeof(this->pcf8563_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u STOP:%s CLKOUT:%0u", pcf8563_.reg.hour_10, + pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second, + pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10, + pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); + + return true; +} + +bool PCF8563Component::write_rtc_() { + if (!this->write_bytes(0, this->pcf8563_.raw, sizeof(this->pcf8563_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u OSC:%s CLKOUT:%0u", pcf8563_.reg.hour_10, + pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second, + pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10, + pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); + return true; +} +} // namespace pcf8563 +} // namespace esphome diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h new file mode 100644 index 0000000000..b6832efe72 --- /dev/null +++ b/esphome/components/pcf8563/pcf8563.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace pcf8563 { + +class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union PCF8563Reg { + struct { + // Control_1 register + bool : 3; + bool power_on_reset : 1; + bool : 1; + bool stop : 1; + bool : 1; + bool ext_test : 1; + + // Control_2 register + bool time_int : 1; + bool alarm_int : 1; + bool timer_flag : 1; + bool alarm_flag : 1; + bool timer_int_timer_pulse : 1; + bool : 3; + + // Seconds register + uint8_t second : 4; + uint8_t second_10 : 3; + bool clock_int : 1; + + // Minutes register + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t : 1; + + // Hours register + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t : 2; + + // Days register + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t : 2; + + // Weekdays register + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + // Months register + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t : 2; + uint8_t century : 1; + + // Years register + uint8_t year : 4; + uint8_t year_10 : 4; + + // Minute Alarm register + uint8_t minute_alarm : 4; + uint8_t minute_alarm_10 : 3; + bool minute_alarm_enabled : 1; + + // Hour Alarm register + uint8_t hour_alarm : 4; + uint8_t hour_alarm_10 : 2; + uint8_t : 1; + bool hour_alarm_enabled : 1; + + // Day Alarm register + uint8_t day_alarm : 4; + uint8_t day_alarm_10 : 2; + uint8_t : 1; + bool day_alarm_enabled : 1; + + // Weekday Alarm register + uint8_t weekday_alarm : 3; + uint8_t : 4; + bool weekday_alarm_enabled : 1; + + // CLKout control register + uint8_t frequency_output : 2; + uint8_t : 5; + uint8_t clkout_enabled : 1; + + // Timer control register + uint8_t timer_source_frequency : 2; + uint8_t : 5; + uint8_t timer_enabled : 1; + + // Timer register + uint8_t countdown_period : 8; + + } reg; + mutable uint8_t raw[sizeof(reg)]; + } pcf8563_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace pcf8563 +} // namespace esphome diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py new file mode 100644 index 0000000000..2e6456a72d --- /dev/null +++ b/esphome/components/pcf8563/time.py @@ -0,0 +1,62 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import i2c, time +from esphome.const import CONF_ID + +CODEOWNERS = ["@KoenBreeman"] + +DEPENDENCIES = ["i2c"] + + +pcf8563_ns = cg.esphome_ns.namespace("pcf8563") +pcf8563Component = pcf8563_ns.class_( + "PCF8563Component", time.RealTimeClock, i2c.I2CDevice +) +WriteAction = pcf8563_ns.class_("WriteAction", automation.Action) +ReadAction = pcf8563_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(pcf8563Component), + } +).extend(i2c.i2c_device_schema(0xA3)) + + +@automation.register_action( + "pcf8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(pcf8563Component), + } + ), +) +async def pcf8563_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "pcf8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(pcf8563Component), + } + ), +) +async def pcf8563_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/tests/test5.yaml b/tests/test5.yaml index cb4b559b06..a2530d799a 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -590,6 +590,7 @@ display: time: - platform: pcf85063 + - platform: pcf8563 text_sensor: - platform: ezo_pmp From 7a551081ee5e08bf71bc52c2c30168992192bb89 Mon Sep 17 00:00:00 2001 From: dentra Date: Wed, 12 Jul 2023 03:08:03 +0300 Subject: [PATCH 11/31] web server esp idf suppport (#3500) * initial web_server_idf implementation * initial web_server_idf implementation * fix lint errors * fix lint errors * add captive_portal support * fix lint errors * fix lint errors * add url decode * Increase the max supported size of headers section in HTTP request * add ota support * add mulipart form data support (ota required) * make linter happy * make linter happy * make linter happy * fix review marks * add DefaultHeaders support * add DefaultHeaders support * unify file names * using std::isnan * parse multipart requests only when ota enabled * parse multipart requests only when ota enabled * parse multipart requests only when ota enabled * parse multipart requests only when ota enabled * parse multipart requests only when ota enabled * drop multipart request support * drop multipart request support * drop multipart request support * OTA is disabled by default * fail when OTA enabled on IDF framework * changing file permissions to remove execute bit * return back PGM_P and strncpy_P macro * temp web_server fix to be compat with 2022.12 * fix config handling w/o web_server * fix compilation with "local" * fully remove all idf ota * merge with esphome 2023.6 * add core/hal to web_server_base * Update esphome/components/web_server_base/__init__.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update __init__.py * Update __init__.py --------- Co-authored-by: Keith Burzinski Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/captive_portal/__init__.py | 12 +- .../captive_portal/captive_portal.cpp | 8 +- .../captive_portal/captive_portal.h | 12 +- esphome/components/web_server/__init__.py | 12 +- .../components/web_server/list_entities.cpp | 4 - esphome/components/web_server/list_entities.h | 4 - esphome/components/web_server/web_server.cpp | 136 ++++--- esphome/components/web_server/web_server.h | 4 - .../components/web_server_base/__init__.py | 23 +- .../web_server_base/web_server_base.cpp | 25 +- .../web_server_base/web_server_base.h | 9 +- esphome/components/web_server_idf/__init__.py | 14 + .../web_server_idf/web_server_idf.cpp | 374 ++++++++++++++++++ .../web_server_idf/web_server_idf.h | 277 +++++++++++++ script/build_codeowners.py | 2 + 16 files changed, 814 insertions(+), 103 deletions(-) create mode 100644 esphome/components/web_server_idf/__init__.py create mode 100644 esphome/components/web_server_idf/web_server_idf.cpp create mode 100644 esphome/components/web_server_idf/web_server_idf.h diff --git a/CODEOWNERS b/CODEOWNERS index 82612fbd4a..8ebf51ceeb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -312,6 +312,7 @@ esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter +esphome/components/web_server_idf/* @dentra esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze esphome/components/wiegand/* @ssieb diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index ff5266e84f..db4a17f6f7 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, cv.only_on(["esp32", "esp8266"]), ) @@ -34,8 +33,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add_define("USE_CAPTIVE_PORTAL") - if CORE.is_esp32: - cg.add_library("DNSServer", None) - cg.add_library("WiFi", None) - if CORE.is_esp8266: - cg.add_library("DNSServer", None) + if CORE.using_arduino: + if CORE.is_esp32: + cg.add_library("DNSServer", None) + cg.add_library("WiFi", None) + if CORE.is_esp8266: + cg.add_library("DNSServer", None) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 3bfdea0ab5..74c6606fc0 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "captive_portal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -46,10 +44,12 @@ void CaptivePortal::start() { this->base_->add_ota_handler(); } +#ifdef USE_ARDUINO this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); this->dns_server_->start(53, "*", (uint32_t) ip); +#endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { @@ -67,7 +67,7 @@ void CaptivePortal::start() { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { - AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); response->addHeader("Content-Encoding", "gzip"); req->send(response); return; @@ -91,5 +91,3 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index c2aada171f..df45d40d12 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,9 +1,9 @@ #pragma once -#ifdef USE_ARDUINO - #include +#ifdef USE_ARDUINO #include +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -18,18 +18,22 @@ class CaptivePortal : public AsyncWebHandler, public Component { CaptivePortal(web_server_base::WebServerBase *base); void setup() override; void dump_config() override; +#ifdef USE_ARDUINO void loop() override { if (this->dns_server_ != nullptr) this->dns_server_->processNextRequest(); } +#endif float get_setup_priority() const override; void start(); bool is_active() const { return this->active_; } void end() { this->active_ = false; this->base_->deinit(); +#ifdef USE_ARDUINO this->dns_server_->stop(); this->dns_server_ = nullptr; +#endif } bool canHandle(AsyncWebServerRequest *request) override { @@ -58,12 +62,12 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; +#ifdef USE_ARDUINO std::unique_ptr dns_server_{nullptr}; +#endif }; extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace captive_portal } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 25c0254f90..ab54ae8582 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -47,6 +47,12 @@ def validate_local(config): return config +def validate_ota(config): + if CORE.using_esp_idf and config[CONF_OTA]: + raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet") + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -71,15 +77,17 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=True): cv.boolean, + cv.SplitDefault( + CONF_OTA, esp8266=True, esp32_arduino=True, esp32_idf=False + ): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, cv.only_on(["esp32", "esp8266"]), default_url, validate_local, + validate_ota, ) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index ce7b4be7f3..016dd37dd9 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "list_entities.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -103,5 +101,3 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 8ddca15edf..1569c8ac57 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" #include "esphome/core/defines.h" @@ -59,5 +57,3 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index eb1858a09c..01057fead6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "web_server.h" #include "esphome/components/json/json_util.h" @@ -9,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_ARDUINO #include "StreamString.h" +#endif #include @@ -181,7 +181,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("")); #endif if (strlen(this->css_url_) > 0) { - stream->print(F("print(F(R"(print(this->css_url_); stream->print(F("\">")); } @@ -381,7 +381,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { std::string state; - if (isnan(value)) { + if (std::isnan(value)) { state = "NA"; } else { state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); @@ -524,11 +524,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); - if (request->hasParam("speed")) { - String speed = request->getParam("speed")->value(); - } if (request->hasParam("speed_level")) { - String speed_level = request->getParam("speed_level")->value(); + auto speed_level = request->getParam("speed_level")->value(); auto val = parse_number(speed_level.c_str()); if (!val.has_value()) { ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str()); @@ -537,7 +534,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc call.set_speed(*val); } if (request->hasParam("oscillation")) { - String speed = request->getParam("oscillation")->value(); + auto speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -585,29 +582,54 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(200); } else if (match.method == "turn_on") { auto call = obj->turn_on(); - if (request->hasParam("brightness")) - call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f); - if (request->hasParam("r")) - call.set_red(request->getParam("r")->value().toFloat() / 255.0f); - if (request->hasParam("g")) - call.set_green(request->getParam("g")->value().toFloat() / 255.0f); - if (request->hasParam("b")) - call.set_blue(request->getParam("b")->value().toFloat() / 255.0f); - if (request->hasParam("white_value")) - call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f); - if (request->hasParam("color_temp")) - call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); - + if (request->hasParam("brightness")) { + auto brightness = parse_number(request->getParam("brightness")->value().c_str()); + if (brightness.has_value()) { + call.set_brightness(*brightness / 255.0f); + } + } + if (request->hasParam("r")) { + auto r = parse_number(request->getParam("r")->value().c_str()); + if (r.has_value()) { + call.set_red(*r / 255.0f); + } + } + if (request->hasParam("g")) { + auto g = parse_number(request->getParam("g")->value().c_str()); + if (g.has_value()) { + call.set_green(*g / 255.0f); + } + } + if (request->hasParam("b")) { + auto b = parse_number(request->getParam("b")->value().c_str()); + if (b.has_value()) { + call.set_blue(*b / 255.0f); + } + } + if (request->hasParam("white_value")) { + auto white_value = parse_number(request->getParam("white_value")->value().c_str()); + if (white_value.has_value()) { + call.set_white(*white_value / 255.0f); + } + } + if (request->hasParam("color_temp")) { + auto color_temp = parse_number(request->getParam("color_temp")->value().c_str()); + if (color_temp.has_value()) { + call.set_color_temperature(*color_temp); + } + } if (request->hasParam("flash")) { - float length_s = request->getParam("flash")->value().toFloat(); - call.set_flash_length(static_cast(length_s * 1000)); + auto flash = parse_number(request->getParam("flash")->value().c_str()); + if (flash.has_value()) { + call.set_flash_length(*flash * 1000); + } } - if (request->hasParam("transition")) { - float length_s = request->getParam("transition")->value().toFloat(); - call.set_transition_length(static_cast(length_s * 1000)); + auto transition = parse_number(request->getParam("transition")->value().c_str()); + if (transition.has_value()) { + call.set_transition_length(*transition * 1000); + } } - if (request->hasParam("effect")) { const char *effect = request->getParam("effect")->value().c_str(); call.set_effect(effect); @@ -618,8 +640,10 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } else if (match.method == "turn_off") { auto call = obj->turn_off(); if (request->hasParam("transition")) { - auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; - call.set_transition_length(length); + auto transition = parse_number(request->getParam("transition")->value().c_str()); + if (transition.has_value()) { + call.set_transition_length(*transition * 1000); + } } this->schedule_([call]() mutable { call.perform(); }); request->send(200); @@ -681,10 +705,18 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) - call.set_position(request->getParam("position")->value().toFloat()); - if (request->hasParam("tilt")) - call.set_tilt(request->getParam("tilt")->value().toFloat()); + if (request->hasParam("position")) { + auto position = parse_number(request->getParam("position")->value().c_str()); + if (position.has_value()) { + call.set_position(*position); + } + } + if (request->hasParam("tilt")) { + auto tilt = parse_number(request->getParam("tilt")->value().c_str()); + if (tilt.has_value()) { + call.set_tilt(*tilt); + } + } this->schedule_([call]() mutable { call.perform(); }); request->send(200); @@ -725,10 +757,9 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); if (request->hasParam("value")) { - String value = request->getParam("value")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_value(*value_f); + auto value = parse_number(request->getParam("value")->value().c_str()); + if (value.has_value()) + call.set_value(*value); } this->schedule_([call]() mutable { call.perform(); }); @@ -747,7 +778,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["step"] = obj->traits.get_step(); root["mode"] = (int) obj->traits.get_mode(); } - if (isnan(value)) { + if (std::isnan(value)) { root["value"] = "\"NaN\""; root["state"] = "NA"; } else { @@ -784,7 +815,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); if (request->hasParam("option")) { - String option = request->getParam("option")->value(); + auto option = request->getParam("option")->value(); call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) } @@ -834,29 +865,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); if (request->hasParam("mode")) { - String mode = request->getParam("mode")->value(); + auto mode = request->getParam("mode")->value(); call.set_mode(mode.c_str()); } if (request->hasParam("target_temperature_high")) { - String value = request->getParam("target_temperature_high")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature_high(*value_f); + auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); + if (target_temperature_high.has_value()) + call.set_target_temperature_high(*target_temperature_high); } if (request->hasParam("target_temperature_low")) { - String value = request->getParam("target_temperature_low")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature_low(*value_f); + auto target_temperature_low = parse_number(request->getParam("target_temperature_low")->value().c_str()); + if (target_temperature_low.has_value()) + call.set_target_temperature_low(*target_temperature_low); } if (request->hasParam("target_temperature")) { - String value = request->getParam("target_temperature")->value(); - optional value_f = parse_number(value.c_str()); - if (value_f.has_value()) - call.set_target_temperature(*value_f); + auto target_temperature = parse_number(request->getParam("target_temperature")->value().c_str()); + if (target_temperature.has_value()) + call.set_target_temperature(*target_temperature); } this->schedule_([call]() mutable { call.perform(); }); @@ -1231,5 +1259,3 @@ void WebServer::schedule_(std::function &&f) { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 83ebba205f..788e30ccf2 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "list_entities.h" #include "esphome/components/web_server_base/web_server_base.h" @@ -291,5 +289,3 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { } // namespace web_server } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 14fb033a56..87f23a990a 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -5,7 +5,15 @@ from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@OttoWinter"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["async_tcp"] + + +def AUTO_LOAD(): + if CORE.using_arduino: + return ["async_tcp"] + if CORE.using_esp_idf: + return ["web_server_idf"] + return [] + web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) @@ -23,9 +31,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.is_esp32: - cg.add_library("WiFi", None) - cg.add_library("FS", None) - cg.add_library("Update", None) - # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0") + if CORE.using_arduino: + if CORE.is_esp32: + cg.add_library("WiFi", None) + cg.add_library("FS", None) + cg.add_library("Update", None) + # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json + cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0") diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 3c269b28b8..997ce0798a 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,16 +1,17 @@ -#ifdef USE_ARDUINO - #include "web_server_base.h" #include "esphome/core/log.h" #include "esphome/core/application.h" -#include +#include "esphome/core/helpers.h" +#ifdef USE_ARDUINO +#include #ifdef USE_ESP32 #include #endif #ifdef USE_ESP8266 #include #endif +#endif namespace esphome { namespace web_server_base { @@ -24,18 +25,22 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } this->handlers_.push_back(handler); - if (this->server_ != nullptr) + if (this->server_ != nullptr) { this->server_->addHandler(handler); + } } void report_ota_error() { +#ifdef USE_ARDUINO StreamString ss; Update.printError(ss); ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str()); +#endif } void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { +#ifdef USE_ARDUINO bool success; if (index == 0) { ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); @@ -45,9 +50,10 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // NOLINTNEXTLINE(readability-static-accessed-through-instance) success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); #endif -#ifdef USE_ESP32 - if (Update.isRunning()) +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + if (Update.isRunning()) { Update.abort(); + } success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); #endif if (!success) { @@ -85,8 +91,10 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin report_ota_error(); } } +#endif } void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { +#ifdef USE_ARDUINO AsyncWebServerResponse *response; if (!Update.hasError()) { response = request->beginResponse(200, "text/plain", "Update Successful!"); @@ -98,10 +106,13 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { } response->addHeader("Connection", "close"); request->send(response); +#endif } void WebServerBase::add_ota_handler() { +#ifdef USE_ARDUINO this->add_handler(new OTARequestHandler(this)); // NOLINT +#endif } float WebServerBase::get_setup_priority() const { // Before WiFi (captive portal) @@ -110,5 +121,3 @@ float WebServerBase::get_setup_priority() const { } // namespace web_server_base } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index ae286b1e22..c312126472 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,14 +1,17 @@ #pragma once -#ifdef USE_ARDUINO - #include #include #include #include "esphome/core/component.h" +#ifdef USE_ARDUINO #include +#elif USE_ESP_IDF +#include "esphome/core/hal.h" +#include "esphome/components/web_server_idf/web_server_idf.h" +#endif namespace esphome { namespace web_server_base { @@ -141,5 +144,3 @@ class OTARequestHandler : public AsyncWebHandler { } // namespace web_server_base } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py new file mode 100644 index 0000000000..bd3db24bc6 --- /dev/null +++ b/esphome/components/web_server_idf/__init__.py @@ -0,0 +1,14 @@ +import esphome.config_validation as cv +from esphome.components.esp32 import add_idf_sdkconfig_option + +CODEOWNERS = ["@dentra"] + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + cv.only_with_esp_idf, +) + + +async def to_code(config): + # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server + add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp new file mode 100644 index 0000000000..444e682460 --- /dev/null +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -0,0 +1,374 @@ +#ifdef USE_ESP_IDF + +#include + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include "esp_tls_crypto.h" + +#include "web_server_idf.h" + +namespace esphome { +namespace web_server_idf { + +#ifndef HTTPD_409 +#define HTTPD_409 "409 Conflict" +#endif + +#define CRLF_STR "\r\n" +#define CRLF_LEN (sizeof(CRLF_STR) - 1) + +static const char *const TAG = "web_server_idf"; + +void AsyncWebServer::end() { + if (this->server_) { + httpd_stop(this->server_); + this->server_ = nullptr; + } +} + +void AsyncWebServer::begin() { + if (this->server_) { + this->end(); + } + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = this->port_; + config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + if (httpd_start(&this->server_, &config) == ESP_OK) { + const httpd_uri_t handler_get = { + .uri = "", + .method = HTTP_GET, + .handler = AsyncWebServer::request_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_get); + + const httpd_uri_t handler_post = { + .uri = "", + .method = HTTP_POST, + .handler = AsyncWebServer::request_handler, + .user_ctx = this, + }; + httpd_register_uri_handler(this->server_, &handler_post); + } +} + +esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) { + ESP_LOGV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri); + AsyncWebServerRequest req(r); + auto *server = static_cast(r->user_ctx); + for (auto *handler : server->handlers_) { + if (handler->canHandle(&req)) { + // At now process only basic requests. + // OTA requires multipart request support and handleUpload for it + handler->handleRequest(&req); + return ESP_OK; + } + } + if (server->on_not_found_) { + server->on_not_found_(&req); + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} + +AsyncWebServerRequest::~AsyncWebServerRequest() { + delete this->rsp_; + for (const auto &pair : this->params_) { + delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +optional AsyncWebServerRequest::get_header(const char *name) const { + size_t buf_len = httpd_req_get_hdr_value_len(*this, name); + if (buf_len == 0) { + return {}; + } + auto buf = std::unique_ptr(new char[++buf_len]); + if (!buf) { + ESP_LOGE(TAG, "No enough memory for get header %s", name); + return {}; + } + if (httpd_req_get_hdr_value_str(*this, name, buf.get(), buf_len) != ESP_OK) { + return {}; + } + return {buf.get()}; +} + +std::string AsyncWebServerRequest::url() const { + auto *str = strchr(this->req_->uri, '?'); + if (str == nullptr) { + return this->req_->uri; + } + return std::string(this->req_->uri, str - this->req_->uri); +} + +std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } + +void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { + httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); +} + +void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { + this->init_response_(nullptr, code, content_type); + if (content) { + httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); + } else { + httpd_resp_send(*this, nullptr, 0); + } +} + +void AsyncWebServerRequest::redirect(const std::string &url) { + httpd_resp_set_status(*this, "302 Found"); + httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_send(*this, nullptr, 0); +} + +void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { + httpd_resp_set_status(*this, code == 200 ? HTTPD_200 + : code == 404 ? HTTPD_404 + : code == 409 ? HTTPD_409 + : to_string(code).c_str()); + + if (content_type && *content_type) { + httpd_resp_set_type(*this, content_type); + } + httpd_resp_set_hdr(*this, "Accept-Ranges", "none"); + + for (const auto &pair : DefaultHeaders::Instance().headers_) { + httpd_resp_set_hdr(*this, pair.first.c_str(), pair.second.c_str()); + } + + delete this->rsp_; + this->rsp_ = rsp; +} + +bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { + if (username == nullptr || password == nullptr || *username == 0) { + return true; + } + auto auth = this->get_header("Authorization"); + if (!auth.has_value()) { + return false; + } + + auto *auth_str = auth.value().c_str(); + + const auto auth_prefix_len = sizeof("Basic ") - 1; + if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) { + ESP_LOGW(TAG, "Only Basic authorization supported yet"); + return false; + } + + std::string user_info; + user_info += username; + user_info += ':'; + user_info += password; + + size_t n = 0, out; + esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast(user_info.c_str()), user_info.size()); + + auto digest = std::unique_ptr(new char[n + 1]); + esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, + reinterpret_cast(user_info.c_str()), user_info.size()); + + return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0; +} + +void AsyncWebServerRequest::requestAuthentication(const char *realm) const { + httpd_resp_set_hdr(*this, "Connection", "keep-alive"); + auto auth_val = str_sprintf("Basic realm=\"%s\"", realm ? realm : "Login Required"); + httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str()); + httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); +} + +static std::string url_decode(const std::string &in) { + std::string out; + out.reserve(in.size()); + for (std::size_t i = 0; i < in.size(); ++i) { + if (in[i] == '%') { + ++i; + if (i + 1 < in.size()) { + auto c = parse_hex(&in[i], 2); + if (c.has_value()) { + out += static_cast(*c); + ++i; + } else { + out += '%'; + out += in[i++]; + out += in[i]; + } + } else { + out += '%'; + out += in[i]; + } + } else if (in[i] == '+') { + out += ' '; + } else { + out += in[i]; + } + } + return out; +} + +AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { + auto find = this->params_.find(name); + if (find != this->params_.end()) { + return find->second; + } + + auto query_len = httpd_req_get_url_query_len(this->req_); + if (query_len == 0) { + return nullptr; + } + + auto query_str = std::unique_ptr(new char[++query_len]); + if (!query_str) { + ESP_LOGE(TAG, "No enough memory for get query param"); + return nullptr; + } + + auto res = httpd_req_get_url_query_str(*this, query_str.get(), query_len); + if (res != ESP_OK) { + ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); + return nullptr; + } + + auto query_val = std::unique_ptr(new char[query_len]); + if (!query_val) { + ESP_LOGE(TAG, "No enough memory for get query param value"); + return nullptr; + } + + res = httpd_query_key_value(query_str.get(), name.c_str(), query_val.get(), query_len); + if (res != ESP_OK) { + this->params_.insert({name, nullptr}); + return nullptr; + } + query_str.release(); + auto decoded = url_decode(query_val.get()); + query_val.release(); + auto *param = new AsyncWebParameter(decoded); // NOLINT(cppcoreguidelines-owning-memory) + this->params_.insert(std::make_pair(name, param)); + return param; +} + +void AsyncWebServerResponse::addHeader(const char *name, const char *value) { + httpd_resp_set_hdr(*this->req_, name, value); +} + +void AsyncResponseStream::print(float value) { this->print(to_string(value)); } + +void AsyncResponseStream::printf(const char *fmt, ...) { + std::string str; + va_list args; + + va_start(args, fmt); + size_t length = vsnprintf(nullptr, 0, fmt, args); + va_end(args); + + str.resize(length); + va_start(args, fmt); + vsnprintf(&str[0], length + 1, fmt, args); + va_end(args); + + this->print(str); +} + +AsyncEventSource::~AsyncEventSource() { + for (auto *ses : this->sessions_) { + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { + auto *rsp = new AsyncEventSourceResponse(request, this); // NOLINT(cppcoreguidelines-owning-memory) + if (this->on_connect_) { + this->on_connect_(rsp); + } + this->sessions_.insert(rsp); +} + +void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + for (auto *ses : this->sessions_) { + ses->send(message, event, id, reconnect); + } +} + +AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server) + : server_(server) { + httpd_req_t *req = *request; + + httpd_resp_set_status(req, HTTPD_200); + httpd_resp_set_type(req, "text/event-stream"); + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + httpd_resp_set_hdr(req, "Connection", "keep-alive"); + + httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN); + + req->sess_ctx = this; + req->free_ctx = AsyncEventSourceResponse::destroy; + + this->hd_ = req->handle; + this->fd_ = httpd_req_to_sockfd(req); +} + +void AsyncEventSourceResponse::destroy(void *ptr) { + auto *rsp = static_cast(ptr); + rsp->server_->sessions_.erase(rsp); + delete rsp; // NOLINT(cppcoreguidelines-owning-memory) +} + +void AsyncEventSourceResponse::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + if (this->fd_ == 0) { + return; + } + + std::string ev; + + if (reconnect) { + ev.append("retry: ", sizeof("retry: ") - 1); + ev.append(to_string(reconnect)); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (id) { + ev.append("id: ", sizeof("id: ") - 1); + ev.append(to_string(id)); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (event && *event) { + ev.append("event: ", sizeof("event: ") - 1); + ev.append(event); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (message && *message) { + ev.append("data: ", sizeof("data: ") - 1); + ev.append(message); + ev.append(CRLF_STR, CRLF_LEN); + } + + if (ev.empty()) { + return; + } + + ev.append(CRLF_STR, CRLF_LEN); + + // Sending chunked content prelude + auto cs = str_snprintf("%x" CRLF_STR, 4 * sizeof(ev.size()) + CRLF_LEN, ev.size()); + httpd_socket_send(this->hd_, this->fd_, cs.c_str(), cs.size(), 0); + + // Sendiing content chunk + httpd_socket_send(this->hd_, this->fd_, ev.c_str(), ev.size(), 0); + + // Indicate end of chunk + httpd_socket_send(this->hd_, this->fd_, CRLF_STR, CRLF_LEN, 0); +} + +} // namespace web_server_idf +} // namespace esphome + +#endif // !defined(USE_ESP_IDF) diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h new file mode 100644 index 0000000000..f3cecca16f --- /dev/null +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -0,0 +1,277 @@ +#pragma once +#ifdef USE_ESP_IDF + +#include + +#include +#include +#include +#include +#include + +namespace esphome { +namespace web_server_idf { + +#define F(string_literal) (string_literal) +#define PGM_P const char * +#define strncpy_P strncpy + +using String = std::string; + +class AsyncWebParameter { + public: + AsyncWebParameter(std::string value) : value_(std::move(value)) {} + const std::string &value() const { return this->value_; } + + protected: + std::string value_; +}; + +class AsyncWebServerRequest; + +class AsyncWebServerResponse { + public: + AsyncWebServerResponse(const AsyncWebServerRequest *req) : req_(req) {} + virtual ~AsyncWebServerResponse() {} + + // NOLINTNEXTLINE(readability-identifier-naming) + void addHeader(const char *name, const char *value); + + virtual const char *get_content_data() const = 0; + virtual size_t get_content_size() const = 0; + + protected: + const AsyncWebServerRequest *req_; +}; + +class AsyncWebServerResponseEmpty : public AsyncWebServerResponse { + public: + AsyncWebServerResponseEmpty(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {} + + const char *get_content_data() const override { return nullptr; }; + size_t get_content_size() const override { return 0; }; +}; + +class AsyncWebServerResponseContent : public AsyncWebServerResponse { + public: + AsyncWebServerResponseContent(const AsyncWebServerRequest *req, std::string content) + : AsyncWebServerResponse(req), content_(std::move(content)) {} + + const char *get_content_data() const override { return this->content_.c_str(); }; + size_t get_content_size() const override { return this->content_.size(); }; + + protected: + std::string content_; +}; + +class AsyncResponseStream : public AsyncWebServerResponse { + public: + AsyncResponseStream(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {} + + const char *get_content_data() const override { return this->content_.c_str(); }; + size_t get_content_size() const override { return this->content_.size(); }; + + void print(const char *str) { this->content_.append(str); } + void print(const std::string &str) { this->content_.append(str); } + void print(float value); + void printf(const char *fmt, ...) __attribute__((format(printf, 2, 3))); + + protected: + std::string content_; +}; + +class AsyncWebServerResponseProgmem : public AsyncWebServerResponse { + public: + AsyncWebServerResponseProgmem(const AsyncWebServerRequest *req, const uint8_t *data, const size_t size) + : AsyncWebServerResponse(req), data_(data), size_(size) {} + + const char *get_content_data() const override { return reinterpret_cast(this->data_); }; + size_t get_content_size() const override { return this->size_; }; + + protected: + const uint8_t *data_; + const size_t size_; +}; + +class AsyncWebServerRequest { + // FIXME friend class AsyncWebServerResponse; + friend class AsyncWebServer; + + public: + ~AsyncWebServerRequest(); + + http_method method() const { return static_cast(this->req_->method); } + std::string url() const; + std::string host() const; + // NOLINTNEXTLINE(readability-identifier-naming) + size_t contentLength() const { return this->req_->content_len; } + + bool authenticate(const char *username, const char *password) const; + // NOLINTNEXTLINE(readability-identifier-naming) + void requestAuthentication(const char *realm = nullptr) const; + + void redirect(const std::string &url); + + void send(AsyncWebServerResponse *response); + void send(int code, const char *content_type = nullptr, const char *content = nullptr); + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse(int code, const char *content_type) { + auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, 200, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse(int code, const char *content_type, const std::string &content) { + auto *res = new AsyncWebServerResponseContent(this, content); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, + const size_t data_size) { + auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, code, content_type); + return res; + } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncResponseStream *beginResponseStream(const char *content_type) { + auto *res = new AsyncResponseStream(this); // NOLINT(cppcoreguidelines-owning-memory) + this->init_response_(res, 200, content_type); + return res; + } + + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const std::string &name); + + // NOLINTNEXTLINE(readability-identifier-naming) + bool hasArg(const char *name) { return this->hasParam(name); } + std::string arg(const std::string &name) { + auto *param = this->getParam(name); + if (param) { + return param->value(); + } + return {}; + } + + operator httpd_req_t *() const { return this->req_; } + optional get_header(const char *name) const; + + protected: + httpd_req_t *req_; + AsyncWebServerResponse *rsp_{}; + std::map params_; + AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} + void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type); +}; + +class AsyncWebHandler; + +class AsyncWebServer { + public: + AsyncWebServer(uint16_t port) : port_(port){}; + ~AsyncWebServer() { this->end(); } + + // NOLINTNEXTLINE(readability-identifier-naming) + void onNotFound(std::function fn) { on_not_found_ = std::move(fn); } + + void begin(); + void end(); + + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebHandler &addHandler(AsyncWebHandler *handler) { + this->handlers_.push_back(handler); + return *handler; + } + + protected: + uint16_t port_{}; + httpd_handle_t server_{}; + static esp_err_t request_handler(httpd_req_t *r); + std::vector handlers_; + std::function on_not_found_{}; +}; + +class AsyncWebHandler { + public: + virtual ~AsyncWebHandler() {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual bool canHandle(AsyncWebServerRequest *request) { return false; } + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleRequest(AsyncWebServerRequest *request) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleUpload(AsyncWebServerRequest *request, const std::string &filename, size_t index, uint8_t *data, + size_t len, bool final) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} + // NOLINTNEXTLINE(readability-identifier-naming) + virtual bool isRequestHandlerTrivial() { return true; } +}; + +class AsyncEventSource; + +class AsyncEventSourceResponse { + friend class AsyncEventSource; + + public: + void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + protected: + AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server); + static void destroy(void *p); + AsyncEventSource *server_; + httpd_handle_t hd_{}; + int fd_{}; +}; + +using AsyncEventSourceClient = AsyncEventSourceResponse; + +class AsyncEventSource : public AsyncWebHandler { + friend class AsyncEventSourceResponse; + using connect_handler_t = std::function; + + public: + AsyncEventSource(std::string url) : url_(std::move(url)) {} + ~AsyncEventSource() override; + + // NOLINTNEXTLINE(readability-identifier-naming) + bool canHandle(AsyncWebServerRequest *request) override { + return request->method() == HTTP_GET && request->url() == this->url_; + } + // NOLINTNEXTLINE(readability-identifier-naming) + void handleRequest(AsyncWebServerRequest *request) override; + // NOLINTNEXTLINE(readability-identifier-naming) + void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); } + + void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + protected: + std::string url_; + std::set sessions_; + connect_handler_t on_connect_{}; +}; + +class DefaultHeaders { + friend class AsyncWebServerRequest; + + public: + // NOLINTNEXTLINE(readability-identifier-naming) + void addHeader(const char *name, const char *value) { this->headers_.emplace_back(name, value); } + + // NOLINTNEXTLINE(readability-identifier-naming) + static DefaultHeaders &Instance() { + static DefaultHeaders instance; + return instance; + } + + protected: + std::vector> headers_; +}; + +} // namespace web_server_idf +} // namespace esphome + +using namespace esphome::web_server_idf; // NOLINT(google-global-names-in-headers) + +#endif // !defined(USE_ESP_IDF) diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 2ee7521b91..22f3c1b4bc 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -7,6 +7,7 @@ from collections import defaultdict from esphome.helpers import write_file_if_changed from esphome.config import get_component, get_platform from esphome.core import CORE +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK parser = argparse.ArgumentParser() parser.add_argument( @@ -38,6 +39,7 @@ parts = [BASE] # Fake some directory so that get_component works CORE.config_path = str(root) +CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None} codeowners = defaultdict(list) From 5f531ac9b03f03d9c4f45cd05360b45bc57dc724 Mon Sep 17 00:00:00 2001 From: Stefan Rado Date: Wed, 12 Jul 2023 03:19:19 +0200 Subject: [PATCH 12/31] Add TT21100 touchscreen component (#4793) Co-authored-by: Rajan Patel Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../touchscreen_binary_sensor.cpp | 5 + .../binary_sensor/touchscreen_binary_sensor.h | 2 +- esphome/components/tt21100/__init__.py | 5 + .../tt21100/binary_sensor/__init__.py | 31 ++++ .../tt21100/binary_sensor/tt21100_button.cpp | 27 +++ .../tt21100/binary_sensor/tt21100_button.h | 28 +++ .../tt21100/touchscreen/__init__.py | 44 +++++ .../tt21100/touchscreen/tt21100.cpp | 175 ++++++++++++++++++ .../components/tt21100/touchscreen/tt21100.h | 49 +++++ tests/test8.yaml | 27 ++- 11 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 esphome/components/tt21100/__init__.py create mode 100644 esphome/components/tt21100/binary_sensor/__init__.py create mode 100644 esphome/components/tt21100/binary_sensor/tt21100_button.cpp create mode 100644 esphome/components/tt21100/binary_sensor/tt21100_button.h create mode 100644 esphome/components/tt21100/touchscreen/__init__.py create mode 100644 esphome/components/tt21100/touchscreen/tt21100.cpp create mode 100644 esphome/components/tt21100/touchscreen/tt21100.h diff --git a/CODEOWNERS b/CODEOWNERS index 8ebf51ceeb..a2ddc84226 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -296,6 +296,7 @@ esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 esphome/components/touchscreen/* @jesserockz esphome/components/tsl2591/* @wjcarpenter +esphome/components/tt21100/* @kroimon esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/number/* @frankiboy1 diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 583392cce3..66df78b62a 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -3,6 +3,11 @@ namespace esphome { namespace touchscreen { +void TouchscreenBinarySensor::setup() { + this->parent_->register_listener(this); + this->publish_initial_state(false); +} + void TouchscreenBinarySensor::touch(TouchPoint tp) { bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index 701468aa1e..b56ae562b1 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -14,7 +14,7 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, public TouchListener, public Parented { public: - void setup() override { this->parent_->register_listener(this); } + void setup() override; /// Set the touch screen area where the button will detect the touch. void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { diff --git a/esphome/components/tt21100/__init__.py b/esphome/components/tt21100/__init__.py new file mode 100644 index 0000000000..a309d34beb --- /dev/null +++ b/esphome/components/tt21100/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@kroimon"] + +tt21100_ns = cg.esphome_ns.namespace("tt21100") diff --git a/esphome/components/tt21100/binary_sensor/__init__.py b/esphome/components/tt21100/binary_sensor/__init__.py new file mode 100644 index 0000000000..d5423a01b2 --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_INDEX + +from .. import tt21100_ns +from ..touchscreen import TT21100Touchscreen, TT21100ButtonListener + +CONF_TT21100_ID = "tt21100_id" + +TT21100Button = tt21100_ns.class_( + "TT21100Button", + binary_sensor.BinarySensor, + cg.Component, + TT21100ButtonListener, + cg.Parented.template(TT21100Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TT21100Button).extend( + { + cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), + cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_TT21100_ID]) + cg.add(var.set_index(config[CONF_INDEX])) diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.cpp b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp new file mode 100644 index 0000000000..2d5ac22a83 --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp @@ -0,0 +1,27 @@ +#include "tt21100_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tt21100 { + +static const char *const TAG = "tt21100.binary_sensor"; + +void TT21100Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void TT21100Button::dump_config() { + LOG_BINARY_SENSOR("", "TT21100 Button", this); + ESP_LOGCONFIG(TAG, " Index: %u", this->index_); +} + +void TT21100Button::update_button(uint8_t index, uint16_t state) { + if (index != this->index_) + return; + + this->publish_state(state > 0); +} + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.h b/esphome/components/tt21100/binary_sensor/tt21100_button.h new file mode 100644 index 0000000000..90b55bb75a --- /dev/null +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/tt21100/touchscreen/tt21100.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tt21100 { + +class TT21100Button : public binary_sensor::BinarySensor, + public Component, + public TT21100ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_index(uint8_t index) { this->index_ = index; } + + void update_button(uint8_t index, uint16_t state) override; + + protected: + uint8_t index_; +}; + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py new file mode 100644 index 0000000000..d96d389e69 --- /dev/null +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +from .. import tt21100_ns + +DEPENDENCIES = ["i2c"] + +TT21100Touchscreen = tt21100_ns.class_( + "TT21100Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) +TT21100ButtonListener = tt21100_ns.class_("TT21100ButtonListener") + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TT21100Touchscreen), + cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x24)) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + + if CONF_RESET_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(rts_pin)) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp new file mode 100644 index 0000000000..28a8c2d754 --- /dev/null +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -0,0 +1,175 @@ +#include "tt21100.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tt21100 { + +static const char *const TAG = "tt21100"; + +static const uint8_t MAX_BUTTONS = 4; +static const uint8_t MAX_TOUCH_POINTS = 5; +static const uint8_t MAX_DATA_LEN = (7 + MAX_TOUCH_POINTS * 10); // 7 Header + (Points * 10 data bytes) + +struct TT21100ButtonReport { + uint16_t length; // Always 14 (0x000E) + uint8_t report_id; // Always 0x03 + uint16_t timestamp; // Number in units of 100 us + uint8_t btn_value; // Only use bit 0..3 + uint16_t btn_signal[MAX_BUTTONS]; +} __attribute__((packed)); + +struct TT21100TouchRecord { + uint8_t : 5; + uint8_t touch_type : 3; + uint8_t tip : 1; + uint8_t event_id : 2; + uint8_t touch_id : 5; + uint16_t x; + uint16_t y; + uint8_t pressure; + uint16_t major_axis_length; + uint8_t orientation; +} __attribute__((packed)); + +struct TT21100TouchReport { + uint16_t length; + uint8_t report_id; + uint16_t timestamp; + uint8_t : 2; + uint8_t large_object : 1; + uint8_t record_num : 5; + uint8_t report_counter : 2; + uint8_t : 3; + uint8_t noise_effect : 3; + TT21100TouchRecord touch_record[MAX_TOUCH_POINTS]; +} __attribute__((packed)); + +void TT21100TouchscreenStore::gpio_intr(TT21100TouchscreenStore *store) { store->touch = true; } + +float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } + +void TT21100Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up TT21100 Touchscreen..."); + + // Register interrupt pin + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->store_.pin = this->interrupt_pin_->to_isr(); + this->interrupt_pin_->attach_interrupt(TT21100TouchscreenStore::gpio_intr, &this->store_, + gpio::INTERRUPT_FALLING_EDGE); + + // Perform reset if necessary + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_(); + } + + // Update display dimensions if they were updated during display setup + this->display_width_ = this->display_->get_width(); + this->display_height_ = this->display_->get_height(); + this->rotation_ = static_cast(this->display_->get_rotation()); + + // Trigger initial read to activate the interrupt + this->store_.touch = true; +} + +void TT21100Touchscreen::loop() { + if (!this->store_.touch) + return; + this->store_.touch = false; + + // Read report length + uint16_t data_len; + this->read((uint8_t *) &data_len, sizeof(data_len)); + + // Read report data + uint8_t data[MAX_DATA_LEN]; + if (data_len > 0 && data_len < sizeof(data)) { + this->read(data, data_len); + + if (data_len == 14) { + // Button event + auto *report = (TT21100ButtonReport *) data; + + ESP_LOGV(TAG, "Button report: Len=%d, ID=%d, Time=%5u, Value=[%u], Signal=[%04X][%04X][%04X][%04X]", + report->length, report->report_id, report->timestamp, report->btn_value, report->btn_signal[0], + report->btn_signal[1], report->btn_signal[2], report->btn_signal[3]); + + for (uint8_t i = 0; i < 4; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, report->btn_signal[i]); + } + + } else if (data_len >= 7) { + // Touch point event + auto *report = (TT21100TouchReport *) data; + + ESP_LOGV(TAG, + "Touch report: Len=%d, ID=%d, Time=%5u, LargeObject=%u, RecordNum=%u, RecordCounter=%u, NoiseEffect=%u", + report->length, report->report_id, report->timestamp, report->large_object, report->record_num, + report->report_counter, report->noise_effect); + + uint8_t touch_count = (data_len - (sizeof(*report) - sizeof(report->touch_record))) / sizeof(TT21100TouchRecord); + + if (touch_count == 0) { + for (auto *listener : this->touch_listeners_) + listener->release(); + return; + } + + for (int i = 0; i < touch_count; i++) { + auto *touch = &report->touch_record[i]; + + ESP_LOGV(TAG, + "Touch %d: Type=%u, Tip=%u, EventId=%u, TouchId=%u, X=%u, Y=%u, Pressure=%u, MajorAxisLen=%u, " + "Orientation=%u", + i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, + touch->pressure, touch->major_axis_length, touch->orientation); + + TouchPoint tp; + switch (this->rotation_) { + case ROTATE_0_DEGREES: + // Origin is top right, so mirror X by default + tp.x = this->display_width_ - touch->x; + tp.y = touch->y; + break; + case ROTATE_90_DEGREES: + tp.x = touch->y; + tp.y = touch->x; + break; + case ROTATE_180_DEGREES: + tp.x = touch->x; + tp.y = this->display_height_ - touch->y; + break; + case ROTATE_270_DEGREES: + tp.x = this->display_height_ - touch->y; + tp.y = this->display_width_ - touch->x; + break; + } + tp.id = touch->tip; + tp.state = touch->pressure; + + this->defer([this, tp]() { this->send_touch_(tp); }); + } + } + } +} + +void TT21100Touchscreen::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void TT21100Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "TT21100 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace tt21100 +} // namespace esphome diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h new file mode 100644 index 0000000000..306360975f --- /dev/null +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tt21100 { + +using namespace touchscreen; + +struct TT21100TouchscreenStore { + volatile bool touch; + ISRInternalGPIOPin pin; + + static void gpio_intr(TT21100TouchscreenStore *store); +}; + +class TT21100ButtonListener { + public: + virtual void update_button(uint8_t index, uint16_t state) = 0; +}; + +class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + void register_button_listener(TT21100ButtonListener *listener) { this->button_listeners_.push_back(listener); } + + protected: + void reset_(); + + TT21100TouchscreenStore store_; + + InternalGPIOPin *interrupt_pin_; + GPIOPin *reset_pin_{nullptr}; + + std::vector button_listeners_; +}; + +} // namespace tt21100 +} // namespace esphome diff --git a/tests/test8.yaml b/tests/test8.yaml index 2430a0d1e6..8d031b033f 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -18,10 +18,35 @@ light: - platform: neopixelbus type: GRB variant: WS2812 - pin: 33 + pin: GPIO38 num_leds: 1 id: neopixel method: esp32_rmt name: neopixel-enable internal: false restore_mode: ALWAYS_OFF + +spi: + clk_pin: GPIO7 + mosi_pin: GPIO6 + +display: + - platform: ili9xxx + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: GPIO48 + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: tt21100 + interrupt_pin: GPIO3 + reset_pin: GPIO48 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 From 6ecc1c14d26657304dd3648468960236044a3ab0 Mon Sep 17 00:00:00 2001 From: kswt Date: Wed, 12 Jul 2023 04:28:48 +0300 Subject: [PATCH 13/31] tuya_light: fix float->int conversion while setting color temperature (#5067) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: kswt --- esphome/components/tuya/light/tuya_light.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 7b7a974de2..66931767b2 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -168,7 +168,7 @@ void TuyaLight::write_state(light::LightState *state) { if (brightness > 0.0f || !color_interlock_) { if (this->color_temperature_id_.has_value()) { - uint32_t color_temp_int = static_cast(color_temperature * this->color_temperature_max_value_); + uint32_t color_temp_int = static_cast(roundf(color_temperature * this->color_temperature_max_value_)); if (this->color_temperature_invert_) { color_temp_int = this->color_temperature_max_value_ - color_temp_int; } From 8a9352939a55ebef4c13cfb55c2d27470f78002c Mon Sep 17 00:00:00 2001 From: Stefan Klug Date: Wed, 12 Jul 2023 03:29:38 +0200 Subject: [PATCH 14/31] Fix typo in mpu6050.cpp (#5086) --- esphome/components/mpu6050/mpu6050.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index 51e3ec2383..64fcd3a2a8 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -77,7 +77,7 @@ void MPU6050Component::setup() { accel_config &= 0b11100111; accel_config |= (MPU6050_RANGE_2G << 3); ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); - if (!this->write_byte(MPU6050_REGISTER_GYRO_CONFIG, gyro_config)) { + if (!this->write_byte(MPU6050_REGISTER_ACCEL_CONFIG, accel_config)) { this->mark_failed(); return; } From cf65bd8ad742f3fe8f6801f89d1a579b0b2d6715 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Tue, 11 Jul 2023 21:38:52 -0400 Subject: [PATCH 15/31] airthings_wave: Battery level reporting (#4979) --- CODEOWNERS | 2 +- .../airthings_wave_base/__init__.py | 60 ++++--- .../airthings_wave_base.cpp | 150 ++++++++++++++++-- .../airthings_wave_base/airthings_wave_base.h | 48 +++++- .../airthings_wave_mini.cpp | 14 +- .../airthings_wave_mini/airthings_wave_mini.h | 5 +- .../airthings_wave_plus.cpp | 14 +- .../airthings_wave_plus/airthings_wave_plus.h | 5 +- .../components/airthings_wave_plus/sensor.py | 12 +- tests/test2.yaml | 6 + 10 files changed, 258 insertions(+), 58 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a2ddc84226..641911e84f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,7 @@ esphome/components/adc/* @esphome/core esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/airthings_ble/* @jeromelaban -esphome/components/airthings_wave_base/* @jeromelaban @ncareau +esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/alarm_control_panel/* @grahambrown11 diff --git a/esphome/components/airthings_wave_base/__init__.py b/esphome/components/airthings_wave_base/__init__.py index c935ce108a..d9b97f1c8d 100644 --- a/esphome/components/airthings_wave_base/__init__.py +++ b/esphome/components/airthings_wave_base/__init__.py @@ -3,26 +3,31 @@ import esphome.config_validation as cv from esphome.components import sensor, ble_client from esphome.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, - UNIT_PERCENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, + CONF_BATTERY_VOLTAGE, CONF_HUMIDITY, - CONF_TVOC, CONF_PRESSURE, CONF_TEMPERATURE, + CONF_TVOC, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, + UNIT_PERCENT, + UNIT_VOLT, ) -CODEOWNERS = ["@ncareau", "@jeromelaban"] +CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"] DEPENDENCIES = ["ble_client"] +CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval" + airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base") AirthingsWaveBase = airthings_wave_base_ns.class_( "AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode @@ -34,9 +39,9 @@ BASE_SCHEMA = ( { cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=0, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, @@ -52,11 +57,21 @@ BASE_SCHEMA = ( ), cv.Optional(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_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional( + CONF_BATTERY_UPDATE_INTERVAL, + default="24h", + ): cv.update_interval, } ) .extend(cv.polling_component_schema("5min")) @@ -69,15 +84,20 @@ async def wave_base_to_code(var, config): await ble_client.register_ble_node(var, config) - if CONF_HUMIDITY in config: - sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + if config_humidity := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(config_humidity) cg.add(var.set_humidity(sens)) - if CONF_TEMPERATURE in config: - sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + if config_temperature := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(config_temperature) cg.add(var.set_temperature(sens)) - if CONF_PRESSURE in config: - sens = await sensor.new_sensor(config[CONF_PRESSURE]) + if config_pressure := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(config_pressure) cg.add(var.set_pressure(sens)) - if CONF_TVOC in config: - sens = await sensor.new_sensor(config[CONF_TVOC]) + if config_tvoc := config.get(CONF_TVOC): + sens = await sensor.new_sensor(config_tvoc) cg.add(var.set_tvoc(sens)) + if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(config_battery_voltage) + cg.add(var.set_battery_voltage(sens)) + if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL): + cg.add(var.set_battery_update_interval(config_battery_update_interval)) diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp index 349d8d58eb..eff466f413 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.cpp +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -1,5 +1,8 @@ #include "airthings_wave_base.h" +// All information related to reading battery information came from the sensors.airthings_wave +// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) + #ifdef USE_ESP32 namespace esphome { @@ -18,22 +21,26 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt } case ESP_GATTC_DISCONNECT_EVT: { + this->handle_ = 0; + this->acp_handle_ = 0; + this->cccd_handle_ = 0; ESP_LOGW(TAG, "Disconnected!"); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { - this->handle_ = 0; - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); - if (chr == nullptr) { - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), - this->sensors_data_characteristic_uuid_.to_string().c_str()); - break; + if (this->request_read_values_()) { + if (!this->read_battery_next_update_) { + this->node_state = espbt::ClientState::ESTABLISHED; + } else { + // delay setting node_state to ESTABLISHED until confirmation of the notify registration + this->request_battery_(); + } } - this->handle_ = chr->handle; - this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; - this->request_read_values_(); + // ensure that the client will be disconnected even if no responses arrive + this->set_response_timeout_(); + break; } @@ -50,6 +57,20 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt break; } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->get_conn_id()) + break; + if (param->notify.handle == this->acp_handle_) { + this->read_battery_(param->notify.value, param->notify.value_len); + } + break; + } + default: break; } @@ -58,7 +79,7 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } void AirthingsWaveBase::update() { - if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { if (!this->parent()->enabled) { ESP_LOGW(TAG, "Reconnecting to device"); this->parent()->set_enabled(true); @@ -69,12 +90,119 @@ void AirthingsWaveBase::update() { } } -void AirthingsWaveBase::request_read_values_() { +bool AirthingsWaveBase::request_read_values_() { + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->sensors_data_characteristic_uuid_.to_string().c_str()); + return false; + } + + this->handle_ = chr->handle; + auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + return false; } + + this->response_pending_(); + return true; +} + +bool AirthingsWaveBase::request_battery_() { + uint8_t battery_command = ACCESS_CONTROL_POINT_COMMAND; + uint8_t cccd_value[2] = {1, 0}; + + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s", + this->service_uuid_.to_string().c_str(), + this->access_control_point_characteristic_uuid_.to_string().c_str()); + return false; + } + + auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_, + CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID); + if (descr == nullptr) { + ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->access_control_point_characteristic_uuid_.to_string().c_str()); + return false; + } + + auto reg_status = + esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), chr->handle); + if (reg_status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", reg_status); + return false; + } + + this->acp_handle_ = chr->handle; + this->cccd_handle_ = descr->handle; + + auto descr_status = + esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->cccd_handle_, + 2, cccd_value, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (descr_status) { + ESP_LOGW(TAG, "Error sending CCC descriptor write request, status=%d", descr_status); + return false; + } + + auto chr_status = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->acp_handle_, 1, + &battery_command, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (chr_status) { + ESP_LOGW(TAG, "Error sending read request for battery, status=%d", chr_status); + return false; + } + + this->response_pending_(); + return true; +} + +void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) { + auto *value = (AccessControlPointResponse *) (&raw_value[2]); + + if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) { + ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery); + + if (this->battery_voltage_ != nullptr) { + float voltage = value->battery / 1000.0f; + + this->battery_voltage_->publish_state(voltage); + } + + // read the battery again at the configured update interval + if (this->battery_update_interval_ != this->update_interval_) { + this->read_battery_next_update_ = false; + this->set_timeout("battery", this->battery_update_interval_, + [this]() { this->read_battery_next_update_ = true; }); + } + } + + this->response_received_(); +} + +void AirthingsWaveBase::response_pending_() { + this->responses_pending_++; + this->set_response_timeout_(); +} + +void AirthingsWaveBase::response_received_() { + if (--this->responses_pending_ == 0) { + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + this->parent()->set_enabled(false); + } +} + +void AirthingsWaveBase::set_response_timeout_() { + this->set_timeout("response_timeout", 30 * 1000, [this]() { + this->responses_pending_ = 1; + this->response_received_(); + }); } } // namespace airthings_wave_base diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.h b/esphome/components/airthings_wave_base/airthings_wave_base.h index 68c0b3497d..1dc2e1f71f 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.h +++ b/esphome/components/airthings_wave_base/airthings_wave_base.h @@ -1,5 +1,8 @@ #pragma once +// All information related to reading battery levels came from the sensors.airthings_wave +// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) + #ifdef USE_ESP32 #include @@ -14,6 +17,11 @@ namespace esphome { namespace airthings_wave_base { +namespace espbt = esphome::esp32_ble_tracker; + +static const uint8_t ACCESS_CONTROL_POINT_COMMAND = 0x6d; +static const auto CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID = espbt::ESPBTUUID::from_uint16(0x2902); + class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode { public: AirthingsWaveBase() = default; @@ -27,21 +35,53 @@ class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientN void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_battery_voltage(sensor::Sensor *voltage) { + battery_voltage_ = voltage; + this->read_battery_next_update_ = true; + } + void set_battery_update_interval(uint32_t interval) { battery_update_interval_ = interval; } protected: bool is_valid_voc_value_(uint16_t voc); - virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0; - void request_read_values_(); + bool request_read_values_(); + virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; uint16_t handle_; - esp32_ble_tracker::ESPBTUUID service_uuid_; - esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID sensors_data_characteristic_uuid_; + + uint16_t acp_handle_{0}; + uint16_t cccd_handle_{0}; + espbt::ESPBTUUID access_control_point_characteristic_uuid_; + + uint8_t responses_pending_{0}; + void response_pending_(); + void response_received_(); + void set_response_timeout_(); + + // default to *not* reading battery voltage from the device; the + // set_* function for the battery sensor will set this to 'true' + bool read_battery_next_update_{false}; + bool request_battery_(); + void read_battery_(uint8_t *raw_value, uint16_t value_len); + uint32_t battery_update_interval_{}; + + struct AccessControlPointResponse { + uint32_t unused1; + uint8_t unused2; + uint8_t illuminance; + uint8_t unused3[10]; + uint16_t unused4[4]; + uint16_t battery; + uint16_t unused5; + }; }; } // namespace airthings_wave_base diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp index 331a13434f..873826d06c 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -26,12 +26,9 @@ void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { if ((this->tvoc_sensor_ != nullptr) && this->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). - this->parent()->set_enabled(false); } + + this->response_received_(); } void AirthingsWaveMini::dump_config() { @@ -42,11 +39,14 @@ void AirthingsWaveMini::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); } AirthingsWaveMini::AirthingsWaveMini() { - this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); - this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->access_control_point_characteristic_uuid_ = + espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); } } // namespace airthings_wave_mini diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h index ec4fd23e60..825ddbdc69 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.h +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h @@ -7,8 +7,11 @@ namespace esphome { namespace airthings_wave_mini { +namespace espbt = esphome::esp32_ble_tracker; + static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-ade7-11e4-89d3-123b93f75cba"; class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { public: @@ -17,7 +20,7 @@ class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { void dump_config() override; protected: - void read_sensors(uint8_t *value, uint16_t value_len) override; + void read_sensors(uint8_t *raw_value, uint16_t value_len) override; struct WaveMiniReadings { uint16_t unused01; diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index acd3a4316d..e44d5fbcaa 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -43,15 +43,12 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { if ((this->tvoc_sensor_ != nullptr) && this->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). - this->parent()->set_enabled(false); } else { ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); } } + + this->response_received_(); } bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } @@ -66,6 +63,7 @@ void AirthingsWavePlus::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); @@ -73,8 +71,10 @@ void AirthingsWavePlus::dump_config() { } AirthingsWavePlus::AirthingsWavePlus() { - this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); - this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); + this->access_control_point_characteristic_uuid_ = + espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); } } // namespace airthings_wave_plus diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index 4acfb9279a..23c8cbb166 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -7,8 +7,11 @@ namespace esphome { namespace airthings_wave_plus { +namespace espbt = esphome::esp32_ble_tracker; + static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba"; class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { public: @@ -24,7 +27,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { bool is_valid_radon_value_(uint16_t radon); bool is_valid_co2_value_(uint16_t co2); - void read_sensors(uint8_t *value, uint16_t value_len) override; + void read_sensors(uint8_t *raw_value, uint16_t value_len) override; sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py index a5903b1d42..643a2bfb68 100644 --- a/esphome/components/airthings_wave_plus/sensor.py +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -53,12 +53,12 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await airthings_wave_base.wave_base_to_code(var, config) - if CONF_RADON in config: - sens = await sensor.new_sensor(config[CONF_RADON]) + if config_radon := config.get(CONF_RADON): + sens = await sensor.new_sensor(config_radon) cg.add(var.set_radon(sens)) - if CONF_RADON_LONG_TERM in config: - sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) + if config_radon_long_term := config.get(CONF_RADON_LONG_TERM): + sens = await sensor.new_sensor(config_radon_long_term) cg.add(var.set_radon_long_term(sens)) - if CONF_CO2 in config: - sens = await sensor.new_sensor(config[CONF_CO2]) + if config_co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(config_co2) cg.add(var.set_co2(sens)) diff --git a/tests/test2.yaml b/tests/test2.yaml index 675fae6cf3..31fd840f5c 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -316,6 +316,7 @@ sensor: platform: airthings_wave_plus ble_client_id: airthings01 update_interval: 5min + battery_update_interval: 12h temperature: name: Wave Plus Temperature radon: @@ -330,10 +331,13 @@ sensor: name: Wave Plus CO2 tvoc: name: Wave Plus VOC + battery_voltage: + name: Wave Plus Battery Voltage - id: airthingswm platform: airthings_wave_mini ble_client_id: airthingsmini01 update_interval: 5min + battery_update_interval: 12h temperature: name: Wave Mini Temperature humidity: @@ -342,6 +346,8 @@ sensor: name: Wave Mini Pressure tvoc: name: Wave Mini VOC + battery_voltage: + name: Wave Mini Battery Voltage - platform: ina260 address: 0x40 current: From e0fd8cd8508eb95acd786a57bb7b5fb2b3c9efd3 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 12 Jul 2023 04:02:53 +0100 Subject: [PATCH 16/31] Add support for Grove tb6612 fng (#4797) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/grove_i2c_motor/__init__.py | 152 +++++++++++++ .../grove_i2c_motor/grove_i2c_motor.cpp | 171 ++++++++++++++ .../grove_i2c_motor/grove_i2c_motor.h | 208 ++++++++++++++++++ tests/test3.1.yaml | 15 ++ 5 files changed, 547 insertions(+) create mode 100644 esphome/components/grove_i2c_motor/__init__.py create mode 100644 esphome/components/grove_i2c_motor/grove_i2c_motor.cpp create mode 100644 esphome/components/grove_i2c_motor/grove_i2c_motor.h diff --git a/CODEOWNERS b/CODEOWNERS index 641911e84f..d9303523df 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -103,6 +103,7 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/grove_i2c_motor/* @max246 esphome/components/growatt_solar/* @leeuwte esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal diff --git a/esphome/components/grove_i2c_motor/__init__.py b/esphome/components/grove_i2c_motor/__init__.py new file mode 100644 index 0000000000..f7888f7293 --- /dev/null +++ b/esphome/components/grove_i2c_motor/__init__.py @@ -0,0 +1,152 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c + +from esphome.const import ( + CONF_ID, + CONF_CHANNEL, + CONF_SPEED, + CONF_DIRECTION, +) + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@max246"] + +grove_i2c_motor_ns = cg.esphome_ns.namespace("grove_i2c_motor") +GROVE_TB6612FNG = grove_i2c_motor_ns.class_( + "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice +) +GROVETB6612FNGMotorRunAction = grove_i2c_motor_ns.class_( + "GROVETB6612FNGMotorRunAction", automation.Action +) +GROVETB6612FNGMotorBrakeAction = grove_i2c_motor_ns.class_( + "GROVETB6612FNGMotorBrakeAction", automation.Action +) +GROVETB6612FNGMotorStopAction = grove_i2c_motor_ns.class_( + "GROVETB6612FNGMotorStopAction", automation.Action +) +GROVETB6612FNGMotorStandbyAction = grove_i2c_motor_ns.class_( + "GROVETB6612FNGMotorStandbyAction", automation.Action +) +GROVETB6612FNGMotorNoStandbyAction = grove_i2c_motor_ns.class_( + "GROVETB6612FNGMotorNoStandbyAction", automation.Action +) + +DIRECTION_TYPE = { + "FORWARD": 1, + "BACKWARD": 2, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(GROVE_TB6612FNG), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x14)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +@automation.register_action( + "grove_i2c_motor.run", + GROVETB6612FNGMotorRunAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + cv.Required(CONF_SPEED): cv.templatable(cv.int_range(min=0, max=255)), + cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), + } + ), +) +async def grove_i2c_motor_run_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) + template_speed = ( + template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed + ) + cg.add(var.set_channel(template_channel)) + cg.add(var.set_speed(template_speed)) + return var + + +@automation.register_action( + "grove_i2c_motor.break", + GROVETB6612FNGMotorBrakeAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_i2c_motor_break_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_i2c_motor.stop", + GROVETB6612FNGMotorStopAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), + } + ), +) +async def grove_i2c_motor_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + cg.add(var.set_channel(template_channel)) + return var + + +@automation.register_action( + "grove_i2c_motor.standby", + GROVETB6612FNGMotorStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_i2c_motor_standby_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var + + +@automation.register_action( + "grove_i2c_motor.no_standby", + GROVETB6612FNGMotorNoStandbyAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + } + ), +) +async def grove_i2c_motor_no_standby_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var diff --git a/esphome/components/grove_i2c_motor/grove_i2c_motor.cpp b/esphome/components/grove_i2c_motor/grove_i2c_motor.cpp new file mode 100644 index 0000000000..669114aec0 --- /dev/null +++ b/esphome/components/grove_i2c_motor/grove_i2c_motor.cpp @@ -0,0 +1,171 @@ +#include "grove_i2c_motor.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace grove_i2c_motor { + +static const char *const TAG = "GroveMotorDriveTB6612FNG"; + +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE = 0x00; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STOP = 0x01; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CW = 0x02; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CCW = 0x03; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY = 0x04; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY = 0x05; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN = 0x06; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP = 0x07; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN = 0x08; +static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR = 0x11; + +void GroveMotorDriveTB6612FNG::dump_config() { + ESP_LOGCONFIG(TAG, "GroveMotorDriveTB6612FNG:"); + LOG_I2C_DEVICE(this); +} + +void GroveMotorDriveTB6612FNG::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grove Motor Drive TB6612FNG ..."); + if (!this->standby()) { + this->mark_failed(); + return; + } +} + +bool GroveMotorDriveTB6612FNG::standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +bool GroveMotorDriveTB6612FNG::not_standby() { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY, &status, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set not standby failed!"); + this->status_set_warning(); + return false; + } + return true; +} + +void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) { + if (addr == 0x00 || addr >= 0x80) { + return; + } + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR, &addr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set new i2c address failed!"); + this->status_set_warning(); + return; + } + this->set_i2c_address(addr); +} + +void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) { + speed = clamp(speed, -255, 255); + + buffer_[0] = channel; + if (speed >= 0) { + buffer_[1] = speed; + } else { + buffer_[1] = (uint8_t) (-speed); + } + + if (speed >= 0) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } else { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run motor failed!"); + this->status_set_warning(); + return; + } + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_brake(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Break motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::dc_motor_stop(uint8_t channel) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STOP, &channel, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Stop dc motor failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm) { + uint8_t cw = 0; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + if (steps > 0) { + cw = 1; + } + // stop + else if (steps == 0) { + this->stepper_stop(); + return; + } else if (steps == INT16_MIN) { + steps = INT16_MAX; + } else { + steps = -steps; + } + + rpm = clamp(rpm, 1, 300); + + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = steps; + buffer_[3] = (steps >> 8); + buffer_[4] = ms_per_step; + buffer_[5] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Run stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_stop() { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Send stop stepper failed!"); + this->status_set_warning(); + return; + } +} + +void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw) { + // 4=>infinite ccw 5=>infinite cw + uint8_t cw = (is_cw) ? 5 : 4; + // 0.1ms_per_step + uint16_t ms_per_step = 0; + + rpm = clamp(rpm, 1, 300); + ms_per_step = (uint16_t) (3000.0 / (float) rpm); + + buffer_[0] = mode; + buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw + buffer_[2] = ms_per_step; + buffer_[3] = (ms_per_step >> 8); + + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN, buffer_, 4) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Write stepper keep run failed"); + this->status_set_warning(); + return; + } +} +} // namespace grove_i2c_motor +} // namespace esphome diff --git a/esphome/components/grove_i2c_motor/grove_i2c_motor.h b/esphome/components/grove_i2c_motor/grove_i2c_motor.h new file mode 100644 index 0000000000..8a0db77e70 --- /dev/null +++ b/esphome/components/grove_i2c_motor/grove_i2c_motor.h @@ -0,0 +1,208 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/automation.h" +//#include "esphome/core/helpers.h" + +/* + Grove_Motor_Driver_TB6612FNG.h + A library for the Grove - Motor Driver(TB6612FNG) + Copyright (c) 2018 seeed technology co., ltd. + Website : www.seeed.cc + Author : Jerry Yip + Create Time: 2018-06 + Version : 0.1 + Change Log : + The MIT License (MIT) + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +namespace esphome { +namespace grove_i2c_motor { + +enum MotorChannelTypeT { + MOTOR_CHA = 0, + MOTOR_CHB = 1, +}; + +enum StepperModeTypeT { + FULL_STEP = 0, + WAVE_DRIVE = 1, + HALF_STEP = 2, + MICRO_STEPPING = 3, +}; + +class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + /************************************************************* + Description + Enter standby mode. Normally you don't need to call this, except that + you have called notStandby() before. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool standby(); + + /************************************************************* + Description + Exit standby mode. Motor driver does't do any action at this mode. + Parameter + Null. + Return + True/False. + *************************************************************/ + bool not_standby(); + + /************************************************************* + Description + Set an new I2C address. + Parameter + addr: 0x01~0x7f + Return + Null. + *************************************************************/ + void set_i2c_addr(uint8_t addr); + + /************************************************************* + Description + Drive a motor. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + speed: -255~255, if speed > 0, motor moves clockwise. + Note that there is always a starting speed(a starting voltage) for motor. + If the input voltage is 5V, the starting speed should larger than 100 or + smaller than -100. + Return + Null. + *************************************************************/ + void dc_motor_run(uint8_t channel, int16_t speed); + + /************************************************************* + Description + Brake, stop the motor immediately + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_brake(uint8_t channel); + + /************************************************************* + Description + Stop the motor slowly. + Parameter + chl: MOTOR_CHA or MOTOR_CHB + Return + Null. + *************************************************************/ + void dc_motor_stop(uint8_t channel); + + /************************************************************* + Description + Drive a stepper. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + steps: The number of steps to run, range from -32768 to 32767. + When steps = 0, the stepper stops. + When steps > 0, the stepper runs clockwise. When steps < 0, the stepper runs anticlockwise. + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + Return + Null. + *************************************************************/ + void stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm); + + /************************************************************* + Description + Stop a stepper. + Parameter + Null. + Return + Null. + *************************************************************/ + void stepper_stop(); + + // keeps moving(direction same as the last move, default to clockwise) + /************************************************************* + Description + Keep a stepper running. + Parameter + mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING, + for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png + rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300. + Note that high rpm will lead to step lose, so rpm should not be larger than 150. + is_cw: Set the running direction, true for clockwise and false for anti-clockwise. + Return + Null. + *************************************************************/ + void stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw); + + private: + uint8_t buffer_[16]; +}; + +template +class GROVETB6612FNGMotorRunAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint16_t, speed) + + void play(Ts... x) override { + auto channel = this->channel_.value(x...); + auto speed = this->speed_.value(x...); + this->parent_->dc_motor_run(channel, speed); + } +}; + +template +class GROVETB6612FNGMotorBrakeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStopAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + + void play(Ts... x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } +}; + +template +class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->standby(); } +}; + +template +class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->not_standby(); } +}; + +} // namespace grove_i2c_motor +} // namespace esphome diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 0daf4e9671..a003c804c9 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -303,6 +303,10 @@ sm2135: rgb_current: 20mA cw_current: 60mA +grove_i2c_motor: + id: test_motor + address: 0x14 + switch: - platform: template name: mpr121_toggle @@ -353,6 +357,17 @@ switch: Content-Type: application/json body: Some data verify_ssl: false + - platform: template + name: open_vent + id: open_vent + optimistic: True + on_turn_on: + then: + - grove_i2c_motor.run: + channel: 1 + speed: 255 + direction: BACKWARD + id: test_motor custom_component: From ec37dece1294b117ed0833772bbc0d1bfdeb8951 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:12:48 +1000 Subject: [PATCH 17/31] Add MCP2515 12MHz xtal support (#5089) --- esphome/components/mcp2515/canbus.py | 1 + esphome/components/mcp2515/mcp2515.cpp | 98 ++++++++++++++++++++--- esphome/components/mcp2515/mcp2515.h | 2 +- esphome/components/mcp2515/mcp2515_defs.h | 56 +++++++++++++ 4 files changed, 143 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp2515/canbus.py b/esphome/components/mcp2515/canbus.py index c410c1af69..4353cd7bc6 100644 --- a/esphome/components/mcp2515/canbus.py +++ b/esphome/components/mcp2515/canbus.py @@ -16,6 +16,7 @@ McpMode = mcp2515_ns.enum("CANCTRL_REQOP_MODE") CAN_CLOCK = { "8MHZ": CanClock.MCP_8MHZ, + "12MHZ": CanClock.MCP_12MHZ, "16MHZ": CanClock.MCP_16MHZ, "20MHZ": CanClock.MCP_20MHZ, } diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index b90b4de66d..fe4a68b583 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -16,11 +16,14 @@ const struct MCP2515::RxBnRegs MCP2515::RXB[N_RXBUFFERS] = {{MCP_RXB0CTRL, MCP_R bool MCP2515::setup_internal() { this->spi_setup(); - if (this->reset_() == canbus::ERROR_FAIL) + if (this->reset_() != canbus::ERROR_OK) return false; - this->set_bitrate_(this->bit_rate_, this->mcp_clock_); - this->set_mode_(this->mcp_mode_); - ESP_LOGV(TAG, "setup done"); + if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) + return false; + if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) + return false; + uint8_t err_flags = this->get_error_flags_(); + ESP_LOGD(TAG, "mcp2515 setup done, error_flags = %02X", err_flags); return true; } @@ -38,7 +41,7 @@ canbus::Error MCP2515::reset_() { set_registers_(MCP_TXB0CTRL, zeros, 14); set_registers_(MCP_TXB1CTRL, zeros, 14); set_registers_(MCP_TXB2CTRL, zeros, 14); - ESP_LOGD(TAG, "reset() CLEARED TXB registers"); + ESP_LOGV(TAG, "reset() CLEARED TXB registers"); set_register_(MCP_RXB0CTRL, 0); set_register_(MCP_RXB1CTRL, 0); @@ -114,16 +117,12 @@ canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); uint32_t end_time = millis() + 10; - bool mode_match = false; while (millis() < end_time) { - uint8_t new_mode = read_register_(MCP_CANSTAT); - new_mode &= CANSTAT_OPMOD; - mode_match = new_mode == mode; - if (mode_match) { - break; - } + if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode) + return canbus::ERROR_OK; } - return mode_match ? canbus::ERROR_OK : canbus::ERROR_FAIL; + ESP_LOGE(TAG, "Failed to set mode"); + return canbus::ERROR_FAIL; } canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { @@ -451,6 +450,78 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo } break; + case (MCP_12MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_12MHZ_5KBPS_CFG1; + cfg2 = MCP_12MHZ_5KBPS_CFG2; + cfg3 = MCP_12MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_12MHZ_10KBPS_CFG1; + cfg2 = MCP_12MHZ_10KBPS_CFG2; + cfg3 = MCP_12MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_12MHZ_20KBPS_CFG1; + cfg2 = MCP_12MHZ_20KBPS_CFG2; + cfg3 = MCP_12MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_12MHZ_33K3BPS_CFG1; + cfg2 = MCP_12MHZ_33K3BPS_CFG2; + cfg3 = MCP_12MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_12MHZ_40KBPS_CFG1; + cfg2 = MCP_12MHZ_40KBPS_CFG2; + cfg3 = MCP_12MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg2 = MCP_12MHZ_50KBPS_CFG2; + cfg3 = MCP_12MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_12MHZ_80KBPS_CFG1; + cfg2 = MCP_12MHZ_80KBPS_CFG2; + cfg3 = MCP_12MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_12MHZ_100KBPS_CFG1; + cfg2 = MCP_12MHZ_100KBPS_CFG2; + cfg3 = MCP_12MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_12MHZ_125KBPS_CFG1; + cfg2 = MCP_12MHZ_125KBPS_CFG2; + cfg3 = MCP_12MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_12MHZ_200KBPS_CFG1; + cfg2 = MCP_12MHZ_200KBPS_CFG2; + cfg3 = MCP_12MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_12MHZ_250KBPS_CFG1; + cfg2 = MCP_12MHZ_250KBPS_CFG2; + cfg3 = MCP_12MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_12MHZ_500KBPS_CFG1; + cfg2 = MCP_12MHZ_500KBPS_CFG2; + cfg3 = MCP_12MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_12MHZ_1000KBPS_CFG1; + cfg2 = MCP_12MHZ_1000KBPS_CFG2; + cfg3 = MCP_12MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + case (MCP_16MHZ): switch (can_speed) { case (canbus::CAN_5KBPS): // 5Kbps @@ -602,6 +673,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo set_register_(MCP_CNF3, cfg3); // NOLINT return canbus::ERROR_OK; } else { + ESP_LOGE(TAG, "Invalid frequency/bitrate combination: %d/%d", can_clock, can_speed); return canbus::ERROR_FAIL; } } diff --git a/esphome/components/mcp2515/mcp2515.h b/esphome/components/mcp2515/mcp2515.h index 3b9797a78a..c77480ce7d 100644 --- a/esphome/components/mcp2515/mcp2515.h +++ b/esphome/components/mcp2515/mcp2515.h @@ -11,7 +11,7 @@ static const uint32_t SPI_CLOCK = 10000000; // 10MHz static const int N_TXBUFFERS = 3; static const int N_RXBUFFERS = 2; -enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_8MHZ }; +enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_12MHZ, MCP_8MHZ }; enum MASK { MASK0, MASK1 }; enum RXF { RXF0 = 0, RXF1 = 1, RXF2 = 2, RXF3 = 3, RXF4 = 4, RXF5 = 5 }; enum RXBn { RXB0 = 0, RXB1 = 1 }; diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h index 454c760c6d..2f5cf2a238 100644 --- a/esphome/components/mcp2515/mcp2515_defs.h +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -207,6 +207,62 @@ static const uint8_t MCP_8MHZ_5KBPS_CFG1 = 0x1F; static const uint8_t MCP_8MHZ_5KBPS_CFG2 = 0xBF; static const uint8_t MCP_8MHZ_5KBPS_CFG3 = 0x87; +/* + * Speed 12M + */ + +static const uint8_t MCP_12MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_1000KBPS_CFG2 = 0x88; +static const uint8_t MCP_12MHZ_1000KBPS_CFG3 = 0x81; + +static const uint8_t MCP_12MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_500KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_250KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_250KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_250KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_200KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_200KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_125KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_125KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_100KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_100KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_12MHZ_80KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_80KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_12MHZ_50KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_50KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_12MHZ_40KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_40KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_33K3BPS_CFG1 = 0x08; +static const uint8_t MCP_12MHZ_33K3BPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_33K3BPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_20KBPS_CFG1 = 0x0E; +static const uint8_t MCP_12MHZ_20KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_20KBPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_10KBPS_CFG1 = 0x31; +static const uint8_t MCP_12MHZ_10KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_10KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_5KBPS_CFG1 = 0x3B; +static const uint8_t MCP_12MHZ_5KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_5KBPS_CFG3 = 0x84; + /* * speed 16M */ From 6d9dbf9e54dc66ac3d8bf0592ddb89a015eb8576 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:22:52 +1000 Subject: [PATCH 18/31] Correct message for standard transmission. (#5088) --- esphome/components/canbus/canbus.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 3fe0d50f06..6316c77ff4 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -30,7 +30,7 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm if (use_extended_id) { ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); } else { - ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); + ESP_LOGD(TAG, "send standard id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); } if (size > CAN_MAX_DATA_LENGTH) size = CAN_MAX_DATA_LENGTH; From 7e52d4f5d64380ef0cf0a8467615122856f925c0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:28:20 +1200 Subject: [PATCH 19/31] Restrict pillow to versions before 10.0.0 (#5090) --- requirements_optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_optional.txt b/requirements_optional.txt index df6b3b387e..8bbf0a6809 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,3 +1,3 @@ -pillow>4.0.0 +pillow>4.0.0,<10.0.0 cairosvg>=2.2.0 cryptography>=2.0.0,<4 From c85f70a236192b5ff8c5c14930509a1938f30b8f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 12 Jul 2023 16:02:37 +1200 Subject: [PATCH 20/31] Bump esphome-dashboard to 20230711.0 (#5085) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c6564d55e0..618fc94e0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==6.1.7 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.3 -esphome-dashboard==20230621.0 +esphome-dashboard==20230711.0 aioesphomeapi==15.0.0 zeroconf==0.69.0 From bbf3d382e8be1a9039f1fc51eb4fd888eb08ee9b Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Wed, 12 Jul 2023 08:12:40 +0400 Subject: [PATCH 21/31] added uart final validate data bits (#5079) --- esphome/components/uart/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index ed60a9f880..aea59d9d8b 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -246,6 +246,7 @@ def final_validate_device_schema( baud_rate: Optional[int] = None, require_tx: bool = False, require_rx: bool = False, + data_bits: Optional[int] = None, parity: Optional[str] = None, stop_bits: Optional[int] = None, ): @@ -268,6 +269,13 @@ def final_validate_device_schema( return validator + def validate_data_bits(value): + if value != data_bits: + raise cv.Invalid( + f"Component {name} requires {data_bits} data bits for the uart bus" + ) + return value + def validate_parity(value): if value != parity: raise cv.Invalid( @@ -278,7 +286,7 @@ def final_validate_device_schema( def validate_stop_bits(value): if value != stop_bits: raise cv.Invalid( - f"Component {name} requires stop bits {stop_bits} for the uart bus" + f"Component {name} requires {stop_bits} stop bits for the uart bus" ) return value @@ -304,6 +312,8 @@ def final_validate_device_schema( ] = validate_pin(CONF_RX_PIN, device) if baud_rate is not None: hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate + if data_bits is not None: + hub_schema[cv.Required(CONF_DATA_BITS)] = validate_data_bits if parity is not None: hub_schema[cv.Required(CONF_PARITY)] = validate_parity if stop_bits is not None: From 8c5978599aa67d358a85e5201ee77b85975b0116 Mon Sep 17 00:00:00 2001 From: danieltwagner Date: Wed, 12 Jul 2023 01:10:22 -0400 Subject: [PATCH 22/31] Add support for ATM90E26 (#4366) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/atm90e26/__init__.py | 1 + esphome/components/atm90e26/atm90e26.cpp | 235 +++++++++++++++++++++ esphome/components/atm90e26/atm90e26.h | 72 +++++++ esphome/components/atm90e26/atm90e26_reg.h | 70 ++++++ esphome/components/atm90e26/sensor.py | 157 ++++++++++++++ tests/test1.yaml | 19 ++ 7 files changed, 555 insertions(+) create mode 100644 esphome/components/atm90e26/__init__.py create mode 100644 esphome/components/atm90e26/atm90e26.cpp create mode 100644 esphome/components/atm90e26/atm90e26.h create mode 100644 esphome/components/atm90e26/atm90e26_reg.h create mode 100644 esphome/components/atm90e26/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d9303523df..f5dc9a17f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,6 +32,7 @@ esphome/components/api/* @OttoWinter esphome/components/as7341/* @mrgnr esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl +esphome/components/atm90e26/* @danieltwagner esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter diff --git a/esphome/components/atm90e26/__init__.py b/esphome/components/atm90e26/__init__.py new file mode 100644 index 0000000000..ac441a9c2d --- /dev/null +++ b/esphome/components/atm90e26/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@danieltwagner"] diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp new file mode 100644 index 0000000000..42a52c4ccf --- /dev/null +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -0,0 +1,235 @@ +#include "atm90e26.h" +#include "atm90e26_reg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e26 { + +static const char *const TAG = "atm90e26"; + +void ATM90E26Component::update() { + if (this->read16_(ATM90E26_REGISTER_FUNCEN) != 0x0030) { + this->status_set_warning(); + return; + } + + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(this->get_line_voltage_()); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(this->get_line_current_()); + } + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(this->get_active_power_()); + } + if (this->reactive_power_sensor_ != nullptr) { + this->reactive_power_sensor_->publish_state(this->get_reactive_power_()); + } + if (this->power_factor_sensor_ != nullptr) { + this->power_factor_sensor_->publish_state(this->get_power_factor_()); + } + if (this->forward_active_energy_sensor_ != nullptr) { + this->forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_()); + } + if (this->reverse_active_energy_sensor_ != nullptr) { + this->reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_()); + } + if (this->freq_sensor_ != nullptr) { + this->freq_sensor_->publish_state(this->get_frequency_()); + } + this->status_clear_warning(); +} + +void ATM90E26Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ATM90E26 Component..."); + this->spi_setup(); + + uint16_t mmode = 0x422; // default values for everything but L/N line current gains + mmode |= (gain_pga_ & 0x7) << 13; + mmode |= (n_line_gain_ & 0x3) << 11; + + this->write16_(ATM90E26_REGISTER_SOFTRESET, 0x789A); // Perform soft reset + this->write16_(ATM90E26_REGISTER_FUNCEN, + 0x0030); // Voltage sag irq=1, report on warnout pin=1, energy dir change irq=0 + uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA); + if (read != 0x0030) { + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read); + this->mark_failed(); + return; + } + // TODO: 100 * * sqrt(2) * / (4 * gain_voltage/32768) + this->write16_(ATM90E26_REGISTER_SAGTH, 0x17DD); // Voltage sag threshhold 0x1F2F + + // Set metering calibration values + this->write16_(ATM90E26_REGISTER_CALSTART, 0x5678); // CAL Metering calibration startup command + + // Configure + this->write16_(ATM90E26_REGISTER_MMODE, mmode); // Metering Mode Configuration (see above) + + this->write16_(ATM90E26_REGISTER_PLCONSTH, (pl_const_ >> 16)); // PL Constant MSB + this->write16_(ATM90E26_REGISTER_PLCONSTL, pl_const_ & 0xFFFF); // PL Constant LSB + + // Calibrate this to be 1 pulse per Wh + this->write16_(ATM90E26_REGISTER_LGAIN, gain_metering_); // L Line Calibration Gain (active power metering) + this->write16_(ATM90E26_REGISTER_LPHI, 0x0000); // L Line Calibration Angle + this->write16_(ATM90E26_REGISTER_NGAIN, 0x0000); // N Line Calibration Gain + this->write16_(ATM90E26_REGISTER_NPHI, 0x0000); // N Line Calibration Angle + this->write16_(ATM90E26_REGISTER_PSTARTTH, 0x08BD); // Active Startup Power Threshold (default) = 2237 + this->write16_(ATM90E26_REGISTER_PNOLTH, 0x0000); // Active No-Load Power Threshold + this->write16_(ATM90E26_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold (default) = 2796 + this->write16_(ATM90E26_REGISTER_QNOLTH, 0x0000); // Reactive No-Load Power Threshold + + // Compute Checksum for the registers we set above + // low byte = sum of all bytes + uint16_t cs = + ((mmode >> 8) + (mmode & 0xFF) + (pl_const_ >> 24) + ((pl_const_ >> 16) & 0xFF) + ((pl_const_ >> 8) & 0xFF) + + (pl_const_ & 0xFF) + (gain_metering_ >> 8) + (gain_metering_ & 0xFF) + 0x08 + 0xBD + 0x0A + 0xEC) & + 0xFF; + // high byte = XOR of all bytes + cs |= ((mmode >> 8) ^ (mmode & 0xFF) ^ (pl_const_ >> 24) ^ ((pl_const_ >> 16) & 0xFF) ^ ((pl_const_ >> 8) & 0xFF) ^ + (pl_const_ & 0xFF) ^ (gain_metering_ >> 8) ^ (gain_metering_ & 0xFF) ^ 0x08 ^ 0xBD ^ 0x0A ^ 0xEC) + << 8; + + this->write16_(ATM90E26_REGISTER_CS1, cs); + ESP_LOGVV(TAG, "Set CS1 to: 0x%04X", cs); + + // Set measurement calibration values + this->write16_(ATM90E26_REGISTER_ADJSTART, 0x5678); // Measurement calibration startup command, registers 31-3A + this->write16_(ATM90E26_REGISTER_UGAIN, gain_voltage_); // Voltage RMS gain + this->write16_(ATM90E26_REGISTER_IGAINL, gain_ct_); // L line current RMS gain + this->write16_(ATM90E26_REGISTER_IGAINN, 0x7530); // N Line Current RMS Gain + this->write16_(ATM90E26_REGISTER_UOFFSET, 0x0000); // Voltage Offset + this->write16_(ATM90E26_REGISTER_IOFFSETL, 0x0000); // L Line Current Offset + this->write16_(ATM90E26_REGISTER_IOFFSETN, 0x0000); // N Line Current Offse + this->write16_(ATM90E26_REGISTER_POFFSETL, 0x0000); // L Line Active Power Offset + this->write16_(ATM90E26_REGISTER_QOFFSETL, 0x0000); // L Line Reactive Power Offset + this->write16_(ATM90E26_REGISTER_POFFSETN, 0x0000); // N Line Active Power Offset + this->write16_(ATM90E26_REGISTER_QOFFSETN, 0x0000); // N Line Reactive Power Offset + + // Compute Checksum for the registers we set above + cs = ((gain_voltage_ >> 8) + (gain_voltage_ & 0xFF) + (gain_ct_ >> 8) + (gain_ct_ & 0xFF) + 0x75 + 0x30) & 0xFF; + cs |= ((gain_voltage_ >> 8) ^ (gain_voltage_ & 0xFF) ^ (gain_ct_ >> 8) ^ (gain_ct_ & 0xFF) ^ 0x75 ^ 0x30) << 8; + this->write16_(ATM90E26_REGISTER_CS2, cs); + ESP_LOGVV(TAG, "Set CS2 to: 0x%04X", cs); + + this->write16_(ATM90E26_REGISTER_CALSTART, + 0x8765); // Checks correctness of 21-2B registers and starts normal metering if ok + this->write16_(ATM90E26_REGISTER_ADJSTART, + 0x8765); // Checks correctness of 31-3A registers and starts normal measurement if ok + + uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS); + if (sys_status & 0xC000) { // Checksum 1 Error + + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X", + this->read16_(ATM90E26_REGISTER_CS1)); + this->mark_failed(); + } + if (sys_status & 0x3000) { // Checksum 2 Error + ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X", + this->read16_(ATM90E26_REGISTER_CS2)); + this->mark_failed(); + } +} + +void ATM90E26Component::dump_config() { + ESP_LOGCONFIG("", "ATM90E26:"); + LOG_PIN(" CS Pin: ", this->cs_); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ATM90E26 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage A", this->voltage_sensor_); + LOG_SENSOR(" ", "Current A", this->current_sensor_); + LOG_SENSOR(" ", "Power A", this->power_sensor_); + LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_sensor_); + LOG_SENSOR(" ", "PF A", this->power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); + LOG_SENSOR(" ", "Frequency", this->freq_sensor_); +} +float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } + +uint16_t ATM90E26Component::read16_(uint8_t a_register) { + uint8_t data[2]; + uint16_t output; + + this->enable(); + delayMicroseconds(4); + this->write_byte(a_register | 0x80); + delayMicroseconds(4); + this->read_array(data, 2); + this->disable(); + + output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output); + return output; +} + +void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) { + ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val); + this->enable(); + delayMicroseconds(4); + this->write_byte(a_register & 0x7F); + delayMicroseconds(4); + this->write_byte((val >> 8) & 0xFF); + this->write_byte(val & 0xFF); + this->disable(); +} + +float ATM90E26Component::get_line_current_() { + uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS); + return current / 1000.0f; +} + +float ATM90E26Component::get_line_voltage_() { + uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS); + return voltage / 100.0f; +} + +float ATM90E26Component::get_active_power_() { + int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement + return (float) val; +} + +float ATM90E26Component::get_reactive_power_() { + int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement + return (float) val; +} + +float ATM90E26Component::get_power_factor_() { + uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed + if (val & 0x8000) { + return -(val & 0x7FF) / 1000.0f; + } else { + return val / 1000.0f; + } +} + +float ATM90E26Component::get_forward_active_energy_() { + uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY); + if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) { + this->cumulative_forward_active_energy_ += val; + } else { + this->cumulative_forward_active_energy_ = val; + } + // The register holds thenths of pulses, we want to output Wh + return (this->cumulative_forward_active_energy_ * 100.0f / meter_constant_); +} + +float ATM90E26Component::get_reverse_active_energy_() { + uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY); + if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) { + this->cumulative_reverse_active_energy_ += val; + } else { + this->cumulative_reverse_active_energy_ = val; + } + return (this->cumulative_reverse_active_energy_ * 100.0f / meter_constant_); +} + +float ATM90E26Component::get_frequency_() { + uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ); + return freq / 100.0f; +} + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h new file mode 100644 index 0000000000..3c098d7e91 --- /dev/null +++ b/esphome/components/atm90e26/atm90e26.h @@ -0,0 +1,72 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace atm90e26 { + +class ATM90E26Component : public PollingComponent, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } + void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; } + void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; } + void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; } + void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; } + void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; } + void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; } + void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_line_freq(int freq) { line_freq_ = freq; } + void set_meter_constant(float val) { meter_constant_ = val; } + void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; } + void set_gain_metering(uint16_t gain) { this->gain_metering_ = gain; } + void set_gain_voltage(uint16_t gain) { this->gain_voltage_ = gain; } + void set_gain_ct(uint16_t gain) { this->gain_ct_ = gain; } + void set_gain_pga(uint16_t gain) { gain_pga_ = gain; } + void set_n_line_gain(uint16_t gain) { n_line_gain_ = gain; } + + protected: + uint16_t read16_(uint8_t a_register); + int read32_(uint8_t addr_h, uint8_t addr_l); + void write16_(uint8_t a_register, uint16_t val); + + float get_line_voltage_(); + float get_line_current_(); + float get_active_power_(); + float get_reactive_power_(); + float get_power_factor_(); + float get_forward_active_energy_(); + float get_reverse_active_energy_(); + float get_frequency_(); + float get_chip_temperature_(); + + sensor::Sensor *freq_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *forward_active_energy_sensor_{nullptr}; + sensor::Sensor *reverse_active_energy_sensor_{nullptr}; + uint32_t cumulative_forward_active_energy_{0}; + uint32_t cumulative_reverse_active_energy_{0}; + uint16_t gain_metering_{7481}; + uint16_t gain_voltage_{26400}; + uint16_t gain_ct_{31251}; + uint16_t gain_pga_{0x4}; + uint16_t n_line_gain_{0x2}; + int line_freq_{60}; + float meter_constant_{3200.0f}; + uint32_t pl_const_{1429876}; +}; + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/atm90e26_reg.h b/esphome/components/atm90e26/atm90e26_reg.h new file mode 100644 index 0000000000..0a925f424e --- /dev/null +++ b/esphome/components/atm90e26/atm90e26_reg.h @@ -0,0 +1,70 @@ +#pragma once + +namespace esphome { +namespace atm90e26 { + +/* Status and Special Register */ +static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00; // Software Reset +static const uint8_t ATM90E26_REGISTER_SYSSTATUS = 0x01; // System Status +static const uint8_t ATM90E26_REGISTER_FUNCEN = 0x02; // Function Enable +static const uint8_t ATM90E26_REGISTER_SAGTH = 0x03; // Voltage Sag Threshold +static const uint8_t ATM90E26_REGISTER_SMALLPMOD = 0x04; // Small-Power Mode +static const uint8_t ATM90E26_REGISTER_LASTDATA = 0x06; // Last Read/Write SPI/UART Value + +/* Metering Calibration and Configuration Register */ +static const uint8_t ATM90E26_REGISTER_LSB = 0x08; // RMS/Power 16-bit LSB +static const uint8_t ATM90E26_REGISTER_CALSTART = 0x20; // Calibration Start Command +static const uint8_t ATM90E26_REGISTER_PLCONSTH = 0x21; // High Word of PL_Constant +static const uint8_t ATM90E26_REGISTER_PLCONSTL = 0x22; // Low Word of PL_Constant +static const uint8_t ATM90E26_REGISTER_LGAIN = 0x23; // L Line Calibration Gain +static const uint8_t ATM90E26_REGISTER_LPHI = 0x24; // L Line Calibration Angle +static const uint8_t ATM90E26_REGISTER_NGAIN = 0x25; // N Line Calibration Gain +static const uint8_t ATM90E26_REGISTER_NPHI = 0x26; // N Line Calibration Angle +static const uint8_t ATM90E26_REGISTER_PSTARTTH = 0x27; // Active Startup Power Threshold +static const uint8_t ATM90E26_REGISTER_PNOLTH = 0x28; // Active No-Load Power Threshold +static const uint8_t ATM90E26_REGISTER_QSTARTTH = 0x29; // Reactive Startup Power Threshold +static const uint8_t ATM90E26_REGISTER_QNOLTH = 0x2A; // Reactive No-Load Power Threshold +static const uint8_t ATM90E26_REGISTER_MMODE = 0x2B; // Metering Mode Configuration +static const uint8_t ATM90E26_REGISTER_CS1 = 0x2C; // Checksum 1 + +/* Measurement Calibration Register */ +static const uint8_t ATM90E26_REGISTER_ADJSTART = 0x30; // Measurement Calibration Start Command +static const uint8_t ATM90E26_REGISTER_UGAIN = 0x31; // Voltage RMS Gain +static const uint8_t ATM90E26_REGISTER_IGAINL = 0x32; // L Line Current RMS Gain +static const uint8_t ATM90E26_REGISTER_IGAINN = 0x33; // N Line Current RMS Gain +static const uint8_t ATM90E26_REGISTER_UOFFSET = 0x34; // Voltage Offset +static const uint8_t ATM90E26_REGISTER_IOFFSETL = 0x35; // L Line Current Offset +static const uint8_t ATM90E26_REGISTER_IOFFSETN = 0x36; // N Line Current Offse +static const uint8_t ATM90E26_REGISTER_POFFSETL = 0x37; // L Line Active Power Offset +static const uint8_t ATM90E26_REGISTER_QOFFSETL = 0x38; // L Line Reactive Power Offset +static const uint8_t ATM90E26_REGISTER_POFFSETN = 0x39; // N Line Active Power Offset +static const uint8_t ATM90E26_REGISTER_QOFFSETN = 0x3A; // N Line Reactive Power Offset +static const uint8_t ATM90E26_REGISTER_CS2 = 0x3B; // Checksum 2 + +/* Energy Register */ +static const uint8_t ATM90E26_REGISTER_APENERGY = 0x40; // Forward Active Energy +static const uint8_t ATM90E26_REGISTER_ANENERGY = 0x41; // Reverse Active Energy +static const uint8_t ATM90E26_REGISTER_ATENERGY = 0x42; // Absolute Active Energy +static const uint8_t ATM90E26_REGISTER_RPENERGY = 0x43; // Forward (Inductive) Reactive Energy +static const uint8_t ATM90E26_REGISTER_RNENERG = 0x44; // Reverse (Capacitive) Reactive Energy +static const uint8_t ATM90E26_REGISTER_RTENERGY = 0x45; // Absolute Reactive Energy +static const uint8_t ATM90E26_REGISTER_ENSTATUS = 0x46; // Metering Status + +/* Measurement Register */ +static const uint8_t ATM90E26_REGISTER_IRMS = 0x48; // L Line Current RMS +static const uint8_t ATM90E26_REGISTER_URMS = 0x49; // Voltage RMS +static const uint8_t ATM90E26_REGISTER_PMEAN = 0x4A; // L Line Mean Active Power +static const uint8_t ATM90E26_REGISTER_QMEAN = 0x4B; // L Line Mean Reactive Power +static const uint8_t ATM90E26_REGISTER_FREQ = 0x4C; // Voltage Frequency +static const uint8_t ATM90E26_REGISTER_POWERF = 0x4D; // L Line Power Factor +static const uint8_t ATM90E26_REGISTER_PANGLE = 0x4E; // Phase Angle between Voltage and L Line Current +static const uint8_t ATM90E26_REGISTER_SMEAN = 0x4F; // L Line Mean Apparent Power +static const uint8_t ATM90E26_REGISTER_IRMS2 = 0x68; // N Line Current rms +static const uint8_t ATM90E26_REGISTER_PMEAN2 = 0x6A; // N Line Mean Active Power +static const uint8_t ATM90E26_REGISTER_QMEAN2 = 0x6B; // N Line Mean Reactive Power +static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D; // N Line Power Factor +static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E; // Phase Angle between Voltage and N Line Current +static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F; // N Line Mean Apparent Power + +} // namespace atm90e26 +} // namespace esphome diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py new file mode 100644 index 0000000000..a0d97ab5ae --- /dev/null +++ b/esphome/components/atm90e26/sensor.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import ( + CONF_ID, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + CONF_POWER_FACTOR, + CONF_FREQUENCY, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_LIGHTBULB, + ICON_CURRENT_AC, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT_HOURS, +) + +CONF_LINE_FREQUENCY = "line_frequency" +CONF_METER_CONSTANT = "meter_constant" +CONF_PL_CONST = "pl_const" +CONF_GAIN_PGA = "gain_pga" +CONF_GAIN_METERING = "gain_metering" +CONF_GAIN_VOLTAGE = "gain_voltage" +CONF_GAIN_CT = "gain_ct" +LINE_FREQS = { + "50HZ": 50, + "60HZ": 60, +} +PGA_GAINS = { + "1X": 0x4, + "4X": 0x0, + "8X": 0x1, + "16X": 0x2, + "24X": 0x3, +} + +atm90e26_ns = cg.esphome_ns.namespace("atm90e26") +ATM90E26Component = atm90e26_ns.class_( + "ATM90E26Component", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ATM90E26Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + icon=ICON_LIGHTBULB, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + 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_TOTAL_INCREASING, + ), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Required(CONF_METER_CONSTANT): cv.positive_float, + cv.Optional(CONF_PL_CONST, default=1429876): cv.uint32_t, + cv.Optional(CONF_GAIN_METERING, default=7481): cv.uint16_t, + cv.Optional(CONF_GAIN_VOLTAGE, default=26400): cv.int_range( + min=0, max=32767 + ), + cv.Optional(CONF_GAIN_CT, default=31251): cv.uint16_t, + cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True), + } + ) + .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_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) + if CONF_REACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER]) + cg.add(var.set_reactive_power_sensor(sens)) + if CONF_POWER_FACTOR in config: + sens = await sensor.new_sensor(config[CONF_POWER_FACTOR]) + cg.add(var.set_power_factor_sensor(sens)) + if CONF_FORWARD_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_FORWARD_ACTIVE_ENERGY]) + cg.add(var.set_forward_active_energy_sensor(sens)) + if CONF_REVERSE_ACTIVE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_REVERSE_ACTIVE_ENERGY]) + cg.add(var.set_reverse_active_energy_sensor(sens)) + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_freq_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT])) + cg.add(var.set_pl_const(config[CONF_PL_CONST])) + cg.add(var.set_gain_metering(config[CONF_GAIN_METERING])) + cg.add(var.set_gain_voltage(config[CONF_GAIN_VOLTAGE])) + cg.add(var.set_gain_ct(config[CONF_GAIN_CT])) + cg.add(var.set_gain_pga(config[CONF_GAIN_PGA])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 9d279b5e40..d0c9801933 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -483,6 +483,25 @@ sensor: nir: name: NIR i2c_id: i2c_bus + - platform: atm90e26 + cs_pin: 5 + voltage: + name: Line Voltage + current: + name: CT Amps + power: + name: Active Watts + power_factor: + name: Power Factor + frequency: + name: Line Frequency + line_frequency: 50Hz + meter_constant: 1000 + pl_const: 1429876 + gain_pga: 1X + gain_metering: 7481 + gain_voltage: 26400 + gain_ct: 31251 - platform: atm90e32 cs_pin: 5 phase_a: From 119bbba2549cb5574e1bb17ae87bec3be8075d0a Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 12 Jul 2023 21:13:50 +0100 Subject: [PATCH 23/31] Grove amend name (#5093) --- CODEOWNERS | 2 +- .../__init__.py | 34 +++++++++---------- .../grove_tb6612fng.cpp} | 6 ++-- .../grove_tb6612fng.h} | 4 +-- tests/test3.1.yaml | 4 +-- 5 files changed, 25 insertions(+), 25 deletions(-) rename esphome/components/{grove_i2c_motor => grove_tb6612fng}/__init__.py (80%) rename esphome/components/{grove_i2c_motor/grove_i2c_motor.cpp => grove_tb6612fng/grove_tb6612fng.cpp} (98%) rename esphome/components/{grove_i2c_motor/grove_i2c_motor.h => grove_tb6612fng/grove_tb6612fng.h} (99%) diff --git a/CODEOWNERS b/CODEOWNERS index f5dc9a17f4..cce94d4ccb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,7 +104,7 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco -esphome/components/grove_i2c_motor/* @max246 +esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal diff --git a/esphome/components/grove_i2c_motor/__init__.py b/esphome/components/grove_tb6612fng/__init__.py similarity index 80% rename from esphome/components/grove_i2c_motor/__init__.py rename to esphome/components/grove_tb6612fng/__init__.py index f7888f7293..75610ce9d3 100644 --- a/esphome/components/grove_i2c_motor/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -14,23 +14,23 @@ DEPENDENCIES = ["i2c"] CODEOWNERS = ["@max246"] -grove_i2c_motor_ns = cg.esphome_ns.namespace("grove_i2c_motor") -GROVE_TB6612FNG = grove_i2c_motor_ns.class_( +grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng") +GROVE_TB6612FNG = grove_tb6612fng_ns.class_( "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice ) -GROVETB6612FNGMotorRunAction = grove_i2c_motor_ns.class_( +GROVETB6612FNGMotorRunAction = grove_tb6612fng_ns.class_( "GROVETB6612FNGMotorRunAction", automation.Action ) -GROVETB6612FNGMotorBrakeAction = grove_i2c_motor_ns.class_( +GROVETB6612FNGMotorBrakeAction = grove_tb6612fng_ns.class_( "GROVETB6612FNGMotorBrakeAction", automation.Action ) -GROVETB6612FNGMotorStopAction = grove_i2c_motor_ns.class_( +GROVETB6612FNGMotorStopAction = grove_tb6612fng_ns.class_( "GROVETB6612FNGMotorStopAction", automation.Action ) -GROVETB6612FNGMotorStandbyAction = grove_i2c_motor_ns.class_( +GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_( "GROVETB6612FNGMotorStandbyAction", automation.Action ) -GROVETB6612FNGMotorNoStandbyAction = grove_i2c_motor_ns.class_( +GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_( "GROVETB6612FNGMotorNoStandbyAction", automation.Action ) @@ -57,7 +57,7 @@ async def to_code(config): @automation.register_action( - "grove_i2c_motor.run", + "grove_tb6612fng.run", GROVETB6612FNGMotorRunAction, cv.Schema( { @@ -68,7 +68,7 @@ async def to_code(config): } ), ) -async def grove_i2c_motor_run_to_code(config, action_id, template_arg, args): +async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -83,7 +83,7 @@ async def grove_i2c_motor_run_to_code(config, action_id, template_arg, args): @automation.register_action( - "grove_i2c_motor.break", + "grove_tb6612fng.break", GROVETB6612FNGMotorBrakeAction, cv.Schema( { @@ -92,7 +92,7 @@ async def grove_i2c_motor_run_to_code(config, action_id, template_arg, args): } ), ) -async def grove_i2c_motor_break_to_code(config, action_id, template_arg, args): +async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -102,7 +102,7 @@ async def grove_i2c_motor_break_to_code(config, action_id, template_arg, args): @automation.register_action( - "grove_i2c_motor.stop", + "grove_tb6612fng.stop", GROVETB6612FNGMotorStopAction, cv.Schema( { @@ -111,7 +111,7 @@ async def grove_i2c_motor_break_to_code(config, action_id, template_arg, args): } ), ) -async def grove_i2c_motor_stop_to_code(config, action_id, template_arg, args): +async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -121,7 +121,7 @@ async def grove_i2c_motor_stop_to_code(config, action_id, template_arg, args): @automation.register_action( - "grove_i2c_motor.standby", + "grove_tb6612fng.standby", GROVETB6612FNGMotorStandbyAction, cv.Schema( { @@ -129,7 +129,7 @@ async def grove_i2c_motor_stop_to_code(config, action_id, template_arg, args): } ), ) -async def grove_i2c_motor_standby_to_code(config, action_id, template_arg, args): +async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -137,7 +137,7 @@ async def grove_i2c_motor_standby_to_code(config, action_id, template_arg, args) @automation.register_action( - "grove_i2c_motor.no_standby", + "grove_tb6612fng.no_standby", GROVETB6612FNGMotorNoStandbyAction, cv.Schema( { @@ -145,7 +145,7 @@ async def grove_i2c_motor_standby_to_code(config, action_id, template_arg, args) } ), ) -async def grove_i2c_motor_no_standby_to_code(config, action_id, template_arg, args): +async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/grove_i2c_motor/grove_i2c_motor.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp similarity index 98% rename from esphome/components/grove_i2c_motor/grove_i2c_motor.cpp rename to esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index 669114aec0..621b7968a4 100644 --- a/esphome/components/grove_i2c_motor/grove_i2c_motor.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -1,9 +1,9 @@ -#include "grove_i2c_motor.h" +#include "grove_tb6612fng.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" namespace esphome { -namespace grove_i2c_motor { +namespace grove_tb6612fng { static const char *const TAG = "GroveMotorDriveTB6612FNG"; @@ -167,5 +167,5 @@ void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t return; } } -} // namespace grove_i2c_motor +} // namespace grove_tb6612fng } // namespace esphome diff --git a/esphome/components/grove_i2c_motor/grove_i2c_motor.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h similarity index 99% rename from esphome/components/grove_i2c_motor/grove_i2c_motor.h rename to esphome/components/grove_tb6612fng/grove_tb6612fng.h index 8a0db77e70..ccdab6472a 100644 --- a/esphome/components/grove_i2c_motor/grove_i2c_motor.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -34,7 +34,7 @@ */ namespace esphome { -namespace grove_i2c_motor { +namespace grove_tb6612fng { enum MotorChannelTypeT { MOTOR_CHA = 0, @@ -204,5 +204,5 @@ class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented void play(Ts... x) override { this->parent_->not_standby(); } }; -} // namespace grove_i2c_motor +} // namespace grove_tb6612fng } // namespace esphome diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index a003c804c9..5f1d3ff28f 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -303,7 +303,7 @@ sm2135: rgb_current: 20mA cw_current: 60mA -grove_i2c_motor: +grove_tb6612fng: id: test_motor address: 0x14 @@ -363,7 +363,7 @@ switch: optimistic: True on_turn_on: then: - - grove_i2c_motor.run: + - grove_tb6612fng.run: channel: 1 speed: 255 direction: BACKWARD From e4a640844ccdf9f9df6aff8adbfdd022a947699a Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Wed, 12 Jul 2023 22:24:49 +0200 Subject: [PATCH 24/31] Fixing colon for tm1637 display if inverted set true (#5072) --- esphome/components/tm1637/tm1637.cpp | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 434c6e65f3..8d7630bd1d 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -300,6 +300,7 @@ uint8_t TM1637Display::read_byte_() { uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { // ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); uint8_t pos = start_pos; + bool use_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') @@ -312,14 +313,14 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { // XABCDEFG, but TM1637 is // XGFEDCBA if (this->inverted_) { // XABCDEFG > XGCBAFED - data = ((data & 0x80) ? 0x80 : 0) | // no move X - ((data & 0x40) ? 0x8 : 0) | // A - ((data & 0x20) ? 0x10 : 0) | // B - ((data & 0x10) ? 0x20 : 0) | // C - ((data & 0x8) ? 0x1 : 0) | // D - ((data & 0x4) ? 0x2 : 0) | // E - ((data & 0x2) ? 0x4 : 0) | // F - ((data & 0x1) ? 0x40 : 0); // G + data = ((data & 0x80) || use_dot ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x8 : 0) | // A + ((data & 0x20) ? 0x10 : 0) | // B + ((data & 0x10) ? 0x20 : 0) | // C + ((data & 0x8) ? 0x1 : 0) | // D + ((data & 0x4) ? 0x2 : 0) | // E + ((data & 0x2) ? 0x4 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G } else { // XABCDEFG > XGFEDCBA data = ((data & 0x80) ? 0x80 : 0) | // no move X @@ -331,18 +332,18 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { ((data & 0x2) ? 0x20 : 0) | // F ((data & 0x1) ? 0x40 : 0); // G } - if (*str == '.') { - if (pos != start_pos) - pos--; - this->buffer_[pos] |= 0b10000000; + use_dot = *str == '.'; + if (use_dot) { + if ((!this->inverted_) && (pos != start_pos)) { + this->buffer_[pos - 1] |= 0b10000000; + } } else { if (pos >= 6) { ESP_LOGE(TAG, "String is too long for the display!"); break; } - this->buffer_[pos] = data; + this->buffer_[pos++] = data; } - pos++; } return pos - start_pos; } From eb859e83f82b48fc2ed311f889dcb90164373e40 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Thu, 13 Jul 2023 00:44:30 +0400 Subject: [PATCH 25/31] Fix use of optional (#5091) --- .../template/binary_sensor/template_binary_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index fce11f63d6..5ce8894a8a 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -11,7 +11,7 @@ void TemplateBinarySensor::setup() { return; if (this->f_ != nullptr) { - this->publish_initial_state(*this->f_()); + this->publish_initial_state(this->f_().value_or(false)); } else { this->publish_initial_state(false); } From a539197bc475e52146597298b7446878a61e9312 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Thu, 13 Jul 2023 00:48:16 +0400 Subject: [PATCH 26/31] New 'Duty Time' sensor component (#5069) --- CODEOWNERS | 1 + esphome/components/duty_time/__init__.py | 1 + .../components/duty_time/duty_time_sensor.cpp | 103 +++++++++++++++ .../components/duty_time/duty_time_sensor.h | 88 +++++++++++++ esphome/components/duty_time/sensor.py | 121 ++++++++++++++++++ tests/test2.yaml | 23 ++++ 6 files changed, 337 insertions(+) create mode 100644 esphome/components/duty_time/__init__.py create mode 100644 esphome/components/duty_time/duty_time_sensor.cpp create mode 100644 esphome/components/duty_time/duty_time_sensor.h create mode 100644 esphome/components/duty_time/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index cce94d4ccb..122dc71b48 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,6 +78,7 @@ esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz esphome/components/ens210/* @itn3rd77 diff --git a/esphome/components/duty_time/__init__.py b/esphome/components/duty_time/__init__.py new file mode 100644 index 0000000000..b708cee80b --- /dev/null +++ b/esphome/components/duty_time/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@dudanov"] diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp new file mode 100644 index 0000000000..045cbcceac --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -0,0 +1,103 @@ +#include "duty_time_sensor.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace duty_time_sensor { + +static const char *const TAG = "duty_time_sensor"; + +void DutyTimeSensor::set_sensor(binary_sensor::BinarySensor *const sensor) { + sensor->add_on_state_callback([this](bool state) { this->process_state_(state); }); +} + +void DutyTimeSensor::start() { + if (!this->last_state_) + this->process_state_(true); +} + +void DutyTimeSensor::stop() { + if (this->last_state_) + this->process_state_(false); +} + +void DutyTimeSensor::update() { + if (this->last_state_) + this->process_state_(true); +} + +void DutyTimeSensor::loop() { + if (this->func_ == nullptr) + return; + + const bool state = this->func_(); + + if (state != this->last_state_) + this->process_state_(state); +} + +void DutyTimeSensor::setup() { + uint32_t seconds = 0; + + if (this->restore_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_.load(&seconds); + } + + this->set_value_(seconds); +} + +void DutyTimeSensor::set_value_(const uint32_t sec) { + this->last_time_ = 0; + if (this->last_state_) + this->last_time_ = millis(); // last time with 0 ms correction + this->publish_and_save_(sec, 0); +} + +void DutyTimeSensor::process_state_(const bool state) { + const uint32_t now = millis(); + + if (this->last_state_) { + // update or falling edge + const uint32_t tm = now - this->last_time_; + const uint32_t ms = tm % 1000; + + this->publish_and_save_(this->total_sec_ + tm / 1000, ms); + this->last_time_ = now - ms; // store time with ms correction + + if (!state) { + // falling edge + this->last_time_ = ms; // temporary store ms correction only + this->last_state_ = false; + + if (this->last_duty_time_sensor_ != nullptr) { + const uint32_t turn_on_ms = now - this->edge_time_; + this->last_duty_time_sensor_->publish_state(turn_on_ms * 1e-3f); + } + } + + } else if (state) { + // rising edge + this->last_time_ = now - this->last_time_; // store time with ms correction + this->edge_time_ = now; // store turn-on start time + this->last_state_ = true; + } +} + +void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) { + this->total_sec_ = sec; + this->publish_state(sec + ms * 1e-3f); + + if (this->restore_) + this->pref_.save(&sec); +} + +void DutyTimeSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Duty Time:"); + ESP_LOGCONFIG(TAG, " Update Interval: %dms", this->get_update_interval()); + ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_)); + LOG_SENSOR(" ", "Duty Time Sensor:", this); + LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); +} + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h new file mode 100644 index 0000000000..27fa383847 --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -0,0 +1,88 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace duty_time_sensor { + +class DutyTimeSensor : public sensor::Sensor, public PollingComponent { + public: + void setup() override; + void update() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void start(); + void stop(); + bool is_running() const { return this->last_state_; } + void reset() { this->set_value_(0); } + + void set_lambda(std::function &&func) { this->func_ = func; } + void set_sensor(binary_sensor::BinarySensor *sensor); + void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; } + void set_restore(bool restore) { this->restore_ = restore; } + + protected: + void set_value_(uint32_t sec); + void process_state_(bool state); + void publish_and_save_(uint32_t sec, uint32_t ms); + + std::function func_{nullptr}; + sensor::Sensor *last_duty_time_sensor_{nullptr}; + ESPPreferenceObject pref_; + + uint32_t total_sec_; + uint32_t last_time_; + uint32_t edge_time_; + bool last_state_{false}; + bool restore_; +}; + +template class StartAction : public Action { + public: + explicit StartAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->start(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class StopAction : public Action { + public: + explicit StopAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->stop(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class ResetAction : public Action { + public: + explicit ResetAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class RunningCondition : public Condition { + public: + explicit RunningCondition(DutyTimeSensor *parent, bool state) : parent_(parent), state_(state) {} + + bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } + + protected: + DutyTimeSensor *parent_; + bool state_; +}; + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py new file mode 100644 index 0000000000..5f8582d481 --- /dev/null +++ b/esphome/components/duty_time/sensor.py @@ -0,0 +1,121 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.automation import ( + Action, + Condition, + maybe_simple_id, + register_action, + register_condition, +) +from esphome.components import binary_sensor, sensor +from esphome.const import ( + CONF_ID, + CONF_SENSOR, + CONF_RESTORE, + CONF_LAMBDA, + UNIT_SECOND, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + DEVICE_CLASS_DURATION, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +CONF_LAST_TIME = "last_time" + +duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor") +DutyTimeSensor = duty_time_sensor_ns.class_( + "DutyTimeSensor", sensor.Sensor, cg.PollingComponent +) +StartAction = duty_time_sensor_ns.class_("StartAction", Action) +StopAction = duty_time_sensor_ns.class_("StopAction", Action) +ResetAction = duty_time_sensor_ns.class_("ResetAction", Action) +SetAction = duty_time_sensor_ns.class_("SetAction", Action) +RunningCondition = duty_time_sensor_ns.class_("RunningCondition", Condition) + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema( + DutyTimeSensor, + unit_of_measurement=UNIT_SECOND, + icon="mdi:timer-play-outline", + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.Optional(CONF_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional(CONF_LAST_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + icon="mdi:timer-marker-outline", + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL, + device_class=DEVICE_CLASS_DURATION, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")), + cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA), +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + cg.add(var.set_restore(config[CONF_RESTORE])) + if CONF_SENSOR in config: + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_) + cg.add(var.set_lambda(lambda_)) + if CONF_LAST_TIME in config: + sens = await sensor.new_sensor(config[CONF_LAST_TIME]) + cg.add(var.set_last_duty_time_sensor(sens)) + + +# AUTOMATIONS + +DUTY_TIME_ID_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(DutyTimeSensor), + } +) + + +@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_start_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) + + +@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_stop_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) + + +@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA) +async def sensor_runtime_reset_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) + + +@register_condition( + "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA +) +async def duty_time_is_running_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, True) + + +@register_condition( + "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA +) +async def duty_time_is_not_running_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, False) diff --git a/tests/test2.yaml b/tests/test2.yaml index 31fd840f5c..291dc240dc 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -416,6 +416,18 @@ sensor: name: Propane test distance battery_level: name: Propane test battery level + - platform: duty_time + id: duty_time1 + name: Test Duty Time + restore: true + last_time: + name: Test Last Duty Time Sensor + sensor: ha_hello_world_binary + - platform: duty_time + id: duty_time2 + name: Test Duty Time 2 + restore: false + lambda: "return true;" time: - platform: homeassistant @@ -423,6 +435,17 @@ time: - at: "16:00:00" then: - logger.log: It's 16:00 + - if: + condition: + - sensor.duty_time.is_running: duty_time2 + then: + - sensor.duty_time.start: duty_time1 + - if: + condition: + - sensor.duty_time.is_not_running: duty_time1 + then: + - sensor.duty_time.stop: duty_time2 + - sensor.duty_time.reset: duty_time1 esp32_touch: setup_mode: true From 9344d85414d26b5b2ff51624dfd6ceb6b8ecd01c Mon Sep 17 00:00:00 2001 From: Lewis Baker Date: Thu, 13 Jul 2023 06:27:45 +0930 Subject: [PATCH 27/31] Fix PIDController::in_deadband() to give correct result when error is zero (#5078) --- esphome/components/pid/pid_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index afc2d91000..30f6038325 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -29,7 +29,7 @@ float PIDController::update(float setpoint, float process_value) { bool PIDController::in_deadband() { // return (fabs(error) < deadband_threshold); float err = -error_; - return ((err > 0 && err < threshold_high_) || (err < 0 && err > threshold_low_)); + return (threshold_low_ < err && err < threshold_high_); } void PIDController::calculate_proportional_term_() { From 844cf316e2410c1f92d67ab32cc549c6064ef12a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:38:24 +1200 Subject: [PATCH 28/31] Edit error message for pillow install to add version restrictions (#5094) --- esphome/components/font/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 29150afe3f..52f877d986 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -67,13 +67,18 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - "(pip install pillow)" + '(pip install pillow">4.0.0,<10.0.0")' ) from err if version.parse(PIL.__version__) < version.parse("4.0.0"): raise cv.Invalid( "Please update your pillow installation to at least 4.0.x. " - "(pip install -U pillow)" + '(pip install pillow">4.0.0,<10.0.0")' + ) + if version.parse(PIL.__version__) >= version.parse("10.0.0"): + raise cv.Invalid( + "Please downgrade your pillow installation to below 10.0.0. " + '(pip install pillow">4.0.0,<10.0.0")' ) return value From 306ab0c56c34792df45c11b8cf0b4cdcfe6970c8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:50:48 +1200 Subject: [PATCH 29/31] Bump version to 2023.8.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3442c392c7..1c4ccdf3a4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.7.0-dev" +__version__ = "2023.8.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ac0549578197ae4c6c7a83a53e1b17a82bcfdb61 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jul 2023 12:19:04 +1200 Subject: [PATCH 30/31] Dont do mqtt ip lookup if `use_address` has ip address (#5096) * Dont do mqtt ip lookup id `use_address` is in config * Fix after actually testing =) --- esphome/__main__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c7c83ad83b..ecf0092b05 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -32,7 +32,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent +from esphome.helpers import indent, is_ip_address from esphome.util import ( run_external_command, run_external_process, @@ -308,8 +308,10 @@ def upload_program(config, args, host): password = ota_conf.get(CONF_PASSWORD, "") if ( - get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED] - ) and CONF_MQTT in config: + not is_ip_address(CORE.address) + and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]) + and CONF_MQTT in config + ): from esphome import mqtt host = mqtt.get_esphome_device_ip( From f8df694aa302ba0f7f935241ea8f4e76c7ddd9f1 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Thu, 13 Jul 2023 02:32:05 +0200 Subject: [PATCH 31/31] Mk2 to prepare color.h for idf >= 5 (#5070) --- esphome/core/color.h | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/esphome/core/color.h b/esphome/core/color.h index 7062a2a8c8..45b2d4c871 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -57,21 +57,6 @@ struct Color { inline bool operator!=(uint32_t colorcode) { // NOLINT return this->raw_32 != colorcode; } - - inline Color &operator=(const Color &rhs) ALWAYS_INLINE { // NOLINT - this->r = rhs.r; - this->g = rhs.g; - this->b = rhs.b; - this->w = rhs.w; - return *this; - } - inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE { - this->w = (colorcode >> 24) & 0xFF; - this->r = (colorcode >> 16) & 0xFF; - this->g = (colorcode >> 8) & 0xFF; - this->b = (colorcode >> 0) & 0xFF; - return *this; - } inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; } inline Color operator*(uint8_t scale) const ALWAYS_INLINE { return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale),