Merge branch 'esphome:dev' into gsm

This commit is contained in:
Olivier ARCHER 2024-07-27 01:30:15 +02:00 committed by GitHub
commit 4d348aae3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
120 changed files with 7676 additions and 4131 deletions

View file

@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.3.0
uses: docker/build-push-action@v6.5.0
with:
context: .
file: ./docker/Dockerfile
@ -69,7 +69,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.3.0
uses: docker/build-push-action@v6.5.0
with:
context: .
file: ./docker/Dockerfile

View file

@ -13,6 +13,13 @@ updates:
schedule:
interval: daily
open-pull-requests-limit: 10
groups:
docker-actions:
applies-to: version-updates
patterns:
- "docker/setup-qemu-action"
- "docker/login-action"
- "docker/setup-buildx-action"
- package-ecosystem: github-actions
directory: "/.github/actions/build-image"
schedule:

View file

@ -46,9 +46,9 @@ jobs:
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
uses: docker/setup-buildx-action@v3.5.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.2.0
- name: Set TAG
run: |

View file

@ -468,6 +468,8 @@ jobs:
- name: Compile config
run: |
. venv/bin/activate
mkdir build_cache
export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
for component in ${{ matrix.components }}; do
./script/test_build_components -e compile -c $component
done

View file

@ -90,18 +90,18 @@ jobs:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
uses: docker/setup-buildx-action@v3.5.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.2.0
- name: Log in to docker hub
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -184,17 +184,17 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
uses: docker/setup-buildx-action@v3.5.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View file

@ -2,6 +2,15 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.4
hooks:
# Run the linter.
- id: ruff
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:

View file

@ -37,6 +37,7 @@ esphome/components/am43/sensor/* @buxtronix
esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
@ -216,6 +217,8 @@ esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_8angle/* @rnauber
esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger

View file

@ -695,7 +695,8 @@ def command_rename(args, config):
os.remove(new_path)
return 1
os.remove(CORE.config_path)
if CORE.config_path != new_path:
os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
print()

View file

@ -1,5 +1,5 @@
import esphome.config_validation as cv
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
CONFIG_SCHEMA = cv.invalid(
"The ade7953 sensor component has been renamed to ade7953_i2c."
)

View file

@ -0,0 +1,4 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
CODEOWNERS = ["@aodrenah"]

View file

@ -0,0 +1,151 @@
// Based on this datasheet:
// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
#include "apds9306.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace apds9306 {
static const char *const TAG = "apds9306";
enum { // APDS9306 registers
APDS9306_MAIN_CTRL = 0x00,
APDS9306_ALS_MEAS_RATE = 0x04,
APDS9306_ALS_GAIN = 0x05,
APDS9306_PART_ID = 0x06,
APDS9306_MAIN_STATUS = 0x07,
APDS9306_CLEAR_DATA_0 = 0x0A, // LSB
APDS9306_CLEAR_DATA_1 = 0x0B,
APDS9306_CLEAR_DATA_2 = 0x0C, // MSB
APDS9306_ALS_DATA_0 = 0x0D, // LSB
APDS9306_ALS_DATA_1 = 0x0E,
APDS9306_ALS_DATA_2 = 0x0F, // MSB
APDS9306_INT_CFG = 0x19,
APDS9306_INT_PERSISTENCE = 0x1A,
APDS9306_ALS_THRES_UP_0 = 0x21, // LSB
APDS9306_ALS_THRES_UP_1 = 0x22,
APDS9306_ALS_THRES_UP_2 = 0x23, // MSB
APDS9306_ALS_THRES_LOW_0 = 0x24, // LSB
APDS9306_ALS_THRES_LOW_1 = 0x25,
APDS9306_ALS_THRES_LOW_2 = 0x26, // MSB
APDS9306_ALS_THRES_VAR = 0x27
};
#define APDS9306_ERROR_CHECK(func, error) \
if (!(func)) { \
ESP_LOGE(TAG, error); \
this->mark_failed(); \
return; \
}
#define APDS9306_WARNING_CHECK(func, warning) \
if (!(func)) { \
ESP_LOGW(TAG, warning); \
this->status_set_warning(); \
return; \
}
#define APDS9306_WRITE_BYTE(reg, value) \
ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \
if (!this->write_byte(reg, value)) { \
ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \
this->mark_failed(); \
return; \
}
void APDS9306::setup() {
ESP_LOGCONFIG(TAG, "Setting up APDS9306...");
uint8_t id;
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065
this->error_code_ = WRONG_ID;
this->mark_failed();
return;
}
// ALS resolution and measurement, see datasheet or init.py for options
uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07);
APDS9306_WRITE_BYTE(APDS9306_ALS_MEAS_RATE, als_meas_rate);
// ALS gain, see datasheet or init.py for options
uint8_t als_gain = (this->gain_ & 0x07);
APDS9306_WRITE_BYTE(APDS9306_ALS_GAIN, als_gain);
// Set to standby mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00);
// Check for data, clear main status
uint8_t status;
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
// Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
ESP_LOGCONFIG(TAG, "APDS9306 setup complete");
}
void APDS9306::dump_config() {
LOG_SENSOR("", "APDS9306", this);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with APDS9306 failed!");
break;
case WRONG_ID:
ESP_LOGE(TAG, "APDS9306 has invalid id!");
break;
default:
ESP_LOGE(TAG, "Setting up APDS9306 registers failed!");
break;
}
}
ESP_LOGCONFIG(TAG, " Gain: %u", AMBIENT_LIGHT_GAIN_VALUES[this->gain_]);
ESP_LOGCONFIG(TAG, " Measurement rate: %u", MEASUREMENT_RATE_VALUES[this->measurement_rate_]);
ESP_LOGCONFIG(TAG, " Measurement Resolution/Bit width: %d", MEASUREMENT_BIT_WIDTH_VALUES[this->bit_width_]);
LOG_UPDATE_INTERVAL(this);
}
void APDS9306::update() {
// Check for new data
uint8_t status;
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
this->status_clear_warning();
if (!(status &= 0b00001000)) { // No new data
return;
}
// Set to standby mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00);
// Clear MAIN STATUS
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
uint8_t als_data[3];
APDS9306_WARNING_CHECK(this->read_bytes(APDS9306_ALS_DATA_0, als_data, 3), "Reading ALS data has failed.");
// Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
uint32_t light_level = 0x00 | encode_uint24(als_data[2], als_data[1], als_data[0]);
float lux = ((float) light_level / AMBIENT_LIGHT_GAIN_VALUES[this->gain_]) *
(100.0f / MEASUREMENT_RATE_VALUES[this->measurement_rate_]);
ESP_LOGD(TAG, "Got illuminance=%.1flx from", lux);
this->publish_state(lux);
}
} // namespace apds9306
} // namespace esphome

View file

@ -0,0 +1,66 @@
// Based on this datasheet:
// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace apds9306 {
enum MeasurementBitWidth : uint8_t {
MEASUREMENT_BIT_WIDTH_20 = 0,
MEASUREMENT_BIT_WIDTH_19 = 1,
MEASUREMENT_BIT_WIDTH_18 = 2,
MEASUREMENT_BIT_WIDTH_17 = 3,
MEASUREMENT_BIT_WIDTH_16 = 4,
MEASUREMENT_BIT_WIDTH_13 = 5,
};
static const uint8_t MEASUREMENT_BIT_WIDTH_VALUES[] = {20, 19, 18, 17, 16, 13};
enum MeasurementRate : uint8_t {
MEASUREMENT_RATE_25 = 0,
MEASUREMENT_RATE_50 = 1,
MEASUREMENT_RATE_100 = 2,
MEASUREMENT_RATE_200 = 3,
MEASUREMENT_RATE_500 = 4,
MEASUREMENT_RATE_1000 = 5,
MEASUREMENT_RATE_2000 = 6,
};
static const uint16_t MEASUREMENT_RATE_VALUES[] = {25, 50, 100, 200, 500, 1000, 2000};
enum AmbientLightGain : uint8_t {
AMBIENT_LIGHT_GAIN_1 = 0,
AMBIENT_LIGHT_GAIN_3 = 1,
AMBIENT_LIGHT_GAIN_6 = 2,
AMBIENT_LIGHT_GAIN_9 = 3,
AMBIENT_LIGHT_GAIN_18 = 4,
};
static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18};
class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void dump_config() override;
void update() override;
void set_bit_width(MeasurementBitWidth bit_width) { this->bit_width_ = bit_width; }
void set_measurement_rate(MeasurementRate measurement_rate) { this->measurement_rate_ = measurement_rate; }
void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; }
protected:
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
WRONG_ID,
} error_code_{NONE};
MeasurementBitWidth bit_width_;
MeasurementRate measurement_rate_;
AmbientLightGain gain_;
};
} // namespace apds9306
} // namespace esphome

View file

@ -0,0 +1,95 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_GAIN,
DEVICE_CLASS_ILLUMINANCE,
ICON_LIGHTBULB,
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
DEPENDENCIES = ["i2c"]
CONF_APDS9306_ID = "apds9306_id"
CONF_BIT_WIDTH = "bit_width"
CONF_MEASUREMENT_RATE = "measurement_rate"
apds9306_ns = cg.esphome_ns.namespace("apds9306")
APDS9306 = apds9306_ns.class_(
"APDS9306", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
MeasurementBitWidth = apds9306_ns.enum("MeasurementBitWidth")
MeasurementRate = apds9306_ns.enum("MeasurementRate")
AmbientLightGain = apds9306_ns.enum("AmbientLightGain")
MEASUREMENT_BIT_WIDTHS = {
20: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_20,
19: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_19,
18: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_18,
17: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_17,
16: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_16,
13: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_13,
}
MEASUREMENT_RATES = {
25: MeasurementRate.MEASUREMENT_RATE_25,
50: MeasurementRate.MEASUREMENT_RATE_50,
100: MeasurementRate.MEASUREMENT_RATE_100,
200: MeasurementRate.MEASUREMENT_RATE_200,
500: MeasurementRate.MEASUREMENT_RATE_500,
1000: MeasurementRate.MEASUREMENT_RATE_1000,
2000: MeasurementRate.MEASUREMENT_RATE_2000,
}
AMBIENT_LIGHT_GAINS = {
1: AmbientLightGain.AMBIENT_LIGHT_GAIN_1,
3: AmbientLightGain.AMBIENT_LIGHT_GAIN_3,
6: AmbientLightGain.AMBIENT_LIGHT_GAIN_6,
9: AmbientLightGain.AMBIENT_LIGHT_GAIN_9,
18: AmbientLightGain.AMBIENT_LIGHT_GAIN_18,
}
def _validate_measurement_rate(value):
value = cv.positive_time_period_milliseconds(value)
return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds)
CONFIG_SCHEMA = (
sensor.sensor_schema(
APDS9306,
unit_of_measurement=UNIT_LUX,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_LIGHTBULB,
)
.extend(
{
cv.Optional(CONF_GAIN, default="1"): cv.enum(AMBIENT_LIGHT_GAINS, int=True),
cv.Optional(CONF_BIT_WIDTH, default="18"): cv.enum(
MEASUREMENT_BIT_WIDTHS, int=True
),
cv.Optional(
CONF_MEASUREMENT_RATE, default="100ms"
): _validate_measurement_rate,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x52))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_bit_width(config[CONF_BIT_WIDTH]))
cg.add(var.set_measurement_rate(config[CONF_MEASUREMENT_RATE]))
cg.add(var.set_ambient_light_gain(config[CONF_GAIN]))

View file

@ -2,6 +2,6 @@ import esphome.config_validation as cv
CODEOWNERS = ["@latonita"]
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
CONFIG_SCHEMA = cv.invalid(
"The bmp3xx sensor component has been renamed to bmp3xx_i2c."
)

View file

@ -2,6 +2,6 @@ import esphome.config_validation as cv
CODEOWNERS = ["@latonita"]
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
CONFIG_SCHEMA = cv.invalid(
"The ens160 sensor component has been renamed to ens160_i2c."
)

View file

@ -7,10 +7,10 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito"]
CONFLICTS_WITH = ["esp32_ble_beacon"]
CONF_BLE_ID = "ble_id"
CONF_IO_CAPABILITY = "io_capability"
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
@ -34,6 +34,19 @@ IO_CAPABILITY = {
"display_yes_no": IoCapability.IO_CAP_IO,
}
esp_power_level_t = cg.global_ns.enum("esp_power_level_t")
TX_POWER_LEVELS = {
-12: esp_power_level_t.ESP_PWR_LVL_N12,
-9: esp_power_level_t.ESP_PWR_LVL_N9,
-6: esp_power_level_t.ESP_PWR_LVL_N6,
-3: esp_power_level_t.ESP_PWR_LVL_N3,
0: esp_power_level_t.ESP_PWR_LVL_N0,
3: esp_power_level_t.ESP_PWR_LVL_P3,
6: esp_power_level_t.ESP_PWR_LVL_P6,
9: esp_power_level_t.ESP_PWR_LVL_P9,
}
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32BLE),
@ -41,6 +54,9 @@ CONFIG_SCHEMA = cv.Schema(
IO_CAPABILITY, lower=True
),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(
CONF_ADVERTISING_CYCLE_TIME, default="10s"
): cv.positive_time_period_milliseconds,
}
).extend(cv.COMPONENT_SCHEMA)
@ -58,6 +74,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
await cg.register_component(var, config)
if CORE.using_esp_idf:

View file

@ -78,6 +78,11 @@ void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &dat
this->advertising_start();
}
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
this->advertising_init_();
this->advertising_->register_raw_advertisement_callback(std::move(callback));
}
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
this->advertising_init_();
this->advertising_->add_service_uuid(uuid);
@ -102,7 +107,7 @@ bool ESP32BLE::ble_pre_setup_() {
void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr)
return;
this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory)
this->advertising_ = new BLEAdvertising(this->advertising_cycle_time_); // NOLINT(cppcoreguidelines-owning-memory)
this->advertising_->set_scan_response(true);
this->advertising_->set_min_preferred_interval(0x06);
@ -312,6 +317,9 @@ void ESP32BLE::loop() {
delete ble_event; // NOLINT(cppcoreguidelines-owning-memory)
ble_event = this->ble_events_.pop();
}
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
}
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {

View file

@ -3,6 +3,8 @@
#include "ble_advertising.h"
#include "ble_uuid.h"
#include <functional>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
@ -76,6 +78,11 @@ class ESP32BLE : public Component {
public:
void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
void set_advertising_cycle_time(uint32_t advertising_cycle_time) {
this->advertising_cycle_time_ = advertising_cycle_time;
}
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
void enable();
void disable();
bool is_active();
@ -89,6 +96,7 @@ class ESP32BLE : public Component {
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_add_service_uuid(ESPBTUUID uuid);
void advertising_remove_service_uuid(ESPBTUUID uuid);
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
@ -121,6 +129,7 @@ class ESP32BLE : public Component {
Queue<BLEEvent> ble_events_;
BLEAdvertising *advertising_;
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_;
bool enable_on_boot_;
};

View file

@ -10,9 +10,9 @@
namespace esphome {
namespace esp32_ble {
static const char *const TAG = "esp32_ble";
static const char *const TAG = "esp32_ble.advertising";
BLEAdvertising::BLEAdvertising() {
BLEAdvertising::BLEAdvertising(uint32_t advertising_cycle_time) : advertising_cycle_time_(advertising_cycle_time) {
this->advertising_data_.set_scan_rsp = false;
this->advertising_data_.include_name = true;
this->advertising_data_.include_txpower = true;
@ -64,7 +64,7 @@ void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
}
}
void BLEAdvertising::start() {
esp_err_t BLEAdvertising::services_advertisement_() {
int num_services = this->advertising_uuids_.size();
if (num_services == 0) {
this->advertising_data_.service_uuid_len = 0;
@ -87,8 +87,8 @@ void BLEAdvertising::start() {
this->advertising_data_.include_txpower = !this->scan_response_;
err = esp_ble_gap_config_adv_data(&this->advertising_data_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err);
return;
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %s", esp_err_to_name(err));
return err;
}
if (this->scan_response_) {
@ -101,8 +101,8 @@ void BLEAdvertising::start() {
this->scan_response_data_.flag = 0;
err = esp_ble_gap_config_adv_data(&this->scan_response_data_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err);
return;
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %s", esp_err_to_name(err));
return err;
}
}
@ -113,8 +113,18 @@ void BLEAdvertising::start() {
err = esp_ble_gap_start_advertising(&this->advertising_params_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err);
return;
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
void BLEAdvertising::start() {
if (this->current_adv_index_ == -1) {
this->services_advertisement_();
} else {
this->raw_advertisements_callbacks_[this->current_adv_index_](true);
}
}
@ -124,6 +134,29 @@ void BLEAdvertising::stop() {
ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err);
return;
}
if (this->current_adv_index_ != -1) {
this->raw_advertisements_callbacks_[this->current_adv_index_](false);
}
}
void BLEAdvertising::loop() {
if (this->raw_advertisements_callbacks_.empty()) {
return;
}
const uint32_t now = millis();
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop();
this->current_adv_index_ += 1;
if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) {
this->current_adv_index_ = -1;
}
this->start();
this->last_advertisement_time_ = now;
}
}
void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
this->raw_advertisements_callbacks_.push_back(std::move(callback));
}
} // namespace esp32_ble

View file

@ -1,20 +1,31 @@
#pragma once
#include <array>
#include <functional>
#include <vector>
#ifdef USE_ESP32
#include <esp_bt.h>
#include <esp_gap_ble_api.h>
#include <esp_gatts_api.h>
namespace esphome {
namespace esp32_ble {
using raw_adv_data_t = struct {
uint8_t *data;
size_t length;
esp_power_level_t power_level;
};
class ESPBTUUID;
class BLEAdvertising {
public:
BLEAdvertising();
BLEAdvertising(uint32_t advertising_cycle_time);
void loop();
void add_service_uuid(ESPBTUUID uuid);
void remove_service_uuid(ESPBTUUID uuid);
@ -22,16 +33,25 @@ class BLEAdvertising {
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_service_data(const std::vector<uint8_t> &data);
void register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void start();
void stop();
protected:
esp_err_t services_advertisement_();
bool scan_response_;
esp_ble_adv_data_t advertising_data_;
esp_ble_adv_data_t scan_response_data_;
esp_ble_adv_params_t advertising_params_;
std::vector<ESPBTUUID> advertising_uuids_;
std::vector<std::function<void(bool)>> raw_advertisements_callbacks_;
const uint32_t advertising_cycle_time_;
uint32_t last_advertisement_time_{0};
int8_t current_adv_index_{-1}; // -1 means standard scan response
};
} // namespace esp32_ble

View file

@ -1,16 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components.esp32_ble import CONF_BLE_ID
from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER
from esphome.core import CORE, TimePeriod
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components import esp32_ble
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
CONFLICTS_WITH = ["esp32_ble_tracker"]
esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon")
ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component)
ESP32BLEBeacon = esp32_ble_beacon_ns.class_(
"ESP32BLEBeacon",
cg.Component,
esp32_ble.GAPEventHandler,
cg.Parented.template(esp32_ble.ESP32BLE),
)
CONF_MAJOR = "major"
CONF_MINOR = "minor"
CONF_MIN_INTERVAL = "min_interval"
@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32BLEBeacon),
cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True),
cv.Required(CONF_UUID): cv.uuid,
cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t,
@ -48,7 +54,7 @@ CONFIG_SCHEMA = cv.All(
min=-128, max=0
),
cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All(
cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True)
cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True)
),
}
).extend(cv.COMPONENT_SCHEMA),
@ -62,6 +68,10 @@ async def to_code(config):
uuid = config[CONF_UUID].hex
uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)]
var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var))
await cg.register_component(var, config)
cg.add(var.set_major(config[CONF_MAJOR]))
cg.add(var.set_minor(config[CONF_MINOR]))

View file

@ -3,14 +3,16 @@
#ifdef USE_ESP32
#include <nvs_flash.h>
#include <freertos/FreeRTOS.h>
#include <esp_bt_main.h>
#include <esp_bt.h>
#include <freertos/task.h>
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#ifdef USE_ARDUINO
#include <esp32-hal-bt.h>
@ -21,20 +23,6 @@ namespace esp32_ble_beacon {
static const char *const TAG = "esp32_ble_beacon";
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static esp_ble_adv_params_t ble_adv_params = {
.adv_int_min = 0x20,
.adv_int_max = 0x40,
.adv_type = ADV_TYPE_NONCONN_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.peer_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
#define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8))
static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = {
.flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}};
@ -53,117 +41,62 @@ void ESP32BLEBeacon::dump_config() {
" UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
", TX Power: %ddBm",
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_,
this->tx_power_);
(this->tx_power_ * 3) - 12);
}
float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32BLEBeacon::setup() {
ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon...");
global_esp32_ble_beacon = this;
this->ble_adv_params_ = {
.adv_int_min = static_cast<uint16_t>(this->min_interval_ / 0.625f),
.adv_int_max = static_cast<uint16_t>(this->max_interval_ / 0.625f),
.adv_type = ADV_TYPE_NONCONN_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.peer_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
xTaskCreatePinnedToCore(ESP32BLEBeacon::ble_core_task,
"ble_task", // name
10000, // stack size (in words)
nullptr, // input params
1, // priority
nullptr, // Handle, not needed
0 // core
);
global_ble->advertising_register_raw_advertisement_callback([this](bool advertise) {
this->advertising_ = advertise;
if (advertise) {
this->on_advertise_();
}
});
}
float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::BLUETOOTH; }
void ESP32BLEBeacon::ble_core_task(void *params) {
ble_setup();
while (true) {
delay(1000); // NOLINT
}
}
void ESP32BLEBeacon::ble_setup() {
ble_adv_params.adv_int_min = static_cast<uint16_t>(global_esp32_ble_beacon->min_interval_ / 0.625f);
ble_adv_params.adv_int_max = static_cast<uint16_t>(global_esp32_ble_beacon->max_interval_ / 0.625f);
// Initialize non-volatile storage for the bluetooth controller
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
return;
}
#ifdef USE_ARDUINO
if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
return;
}
#else
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
err = esp_bt_controller_init(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err));
return;
}
while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE)
;
}
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) {
err = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err));
return;
}
}
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
ESP_LOGE(TAG, "esp bt controller enable failed");
return;
}
}
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
err = esp_bluedroid_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err);
return;
}
err = esp_bluedroid_enable();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
return;
}
err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV,
static_cast<esp_power_level_t>((global_esp32_ble_beacon->tx_power_ + 12) / 3));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
return;
}
err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
return;
}
void ESP32BLEBeacon::on_advertise_() {
esp_ble_ibeacon_t ibeacon_adv_data;
memcpy(&ibeacon_adv_data.ibeacon_head, &IBEACON_COMMON_HEAD, sizeof(esp_ble_ibeacon_head_t));
memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, global_esp32_ble_beacon->uuid_.data(),
memcpy(&ibeacon_adv_data.ibeacon_vendor.proximity_uuid, this->uuid_.data(),
sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid));
ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_);
ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_);
ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(global_esp32_ble_beacon->measured_power_);
ibeacon_adv_data.ibeacon_vendor.minor = byteswap(this->minor_);
ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_);
ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_);
esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
ESP_LOGD(TAG, "Setting BLE TX power");
esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
}
err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err));
return;
}
}
void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
if (!this->advertising_)
return;
esp_err_t err;
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {
err = esp_ble_gap_start_advertising(&ble_adv_params);
err = esp_ble_gap_start_advertising(&this->ble_adv_params_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err);
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err));
}
break;
}
@ -181,6 +114,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap
} else {
ESP_LOGD(TAG, "BLE stopped advertising successfully");
}
// this->advertising_ = false;
break;
}
default:
@ -188,8 +122,6 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap
}
}
ESP32BLEBeacon *global_esp32_ble_beacon = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esp32_ble_beacon
} // namespace esphome

View file

@ -1,39 +1,39 @@
#pragma once
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32
#include <esp_gap_ble_api.h>
#include <esp_bt.h>
#include <esp_gap_ble_api.h>
namespace esphome {
namespace esp32_ble_beacon {
// NOLINTNEXTLINE(modernize-use-using)
typedef struct {
using esp_ble_ibeacon_head_t = struct {
uint8_t flags[3];
uint8_t length;
uint8_t type;
uint8_t company_id[2];
uint8_t beacon_type[2];
} __attribute__((packed)) esp_ble_ibeacon_head_t;
} __attribute__((packed));
// NOLINTNEXTLINE(modernize-use-using)
typedef struct {
using esp_ble_ibeacon_vendor_t = struct {
uint8_t proximity_uuid[16];
uint16_t major;
uint16_t minor;
uint8_t measured_power;
} __attribute__((packed)) esp_ble_ibeacon_vendor_t;
} __attribute__((packed));
// NOLINTNEXTLINE(modernize-use-using)
typedef struct {
using esp_ble_ibeacon_t = struct {
esp_ble_ibeacon_head_t ibeacon_head;
esp_ble_ibeacon_vendor_t ibeacon_vendor;
} __attribute__((packed)) esp_ble_ibeacon_t;
} __attribute__((packed));
class ESP32BLEBeacon : public Component {
using namespace esp32_ble;
class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented<ESP32BLE> {
public:
explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {}
@ -46,12 +46,11 @@ class ESP32BLEBeacon : public Component {
void set_min_interval(uint16_t val) { this->min_interval_ = val; }
void set_max_interval(uint16_t val) { this->max_interval_ = val; }
void set_measured_power(int8_t val) { this->measured_power_ = val; }
void set_tx_power(int8_t val) { this->tx_power_ = val; }
void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; }
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
protected:
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
static void ble_core_task(void *params);
static void ble_setup();
void on_advertise_();
std::array<uint8_t, 16> uuid_;
uint16_t major_{};
@ -59,12 +58,11 @@ class ESP32BLEBeacon : public Component {
uint16_t min_interval_{};
uint16_t max_interval_{};
int8_t measured_power_{};
int8_t tx_power_{};
esp_power_level_t tx_power_{};
esp_ble_adv_params_t ble_adv_params_;
bool advertising_{false};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32BLEBeacon *global_esp32_ble_beacon;
} // namespace esp32_ble_beacon
} // namespace esphome

View file

@ -7,7 +7,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
AUTO_LOAD = ["esp32_ble"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
CONFLICTS_WITH = ["esp32_ble_beacon"]
DEPENDENCIES = ["esp32"]
CONF_MANUFACTURER = "manufacturer"

View file

@ -6,7 +6,6 @@ from esphome.const import CONF_ID
AUTO_LOAD = ["esp32_ble_server"]
CODEOWNERS = ["@jesserockz"]
CONFLICTS_WITH = ["esp32_ble_beacon"]
DEPENDENCIES = ["wifi", "esp32"]
CONF_AUTHORIZED_DURATION = "authorized_duration"

View file

@ -45,8 +45,8 @@ void FanCall::validate_() {
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
if (this->binary_state_.has_value() && *this->binary_state_) {
// when turning on, if current speed is zero, set speed to 100%
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0) {
// when turning on, if neither current nor new speed available, set speed to 100%
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) {
this->speed_ = traits.supported_speed_count();
}
}

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_PROTOCOL,
CONF_VISUAL,
)
from esphome.core import CORE
CODEOWNERS = ["@rob-deutsch"]
@ -127,3 +128,5 @@ def to_code(config):
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.27")
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")

View file

@ -1,17 +1,17 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import (
__version__,
CONF_ESP8266_DISABLE_SSL_SUPPORT,
CONF_ID,
CONF_TIMEOUT,
CONF_METHOD,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_URL,
CONF_ESP8266_DISABLE_SSL_SUPPORT,
__version__,
)
from esphome.core import Lambda, CORE
from esphome.components import esp32
from esphome.core import CORE, Lambda
DEPENDENCIES = ["network"]
AUTO_LOAD = ["json"]
@ -40,6 +40,8 @@ CONF_VERIFY_SSL = "verify_ssl"
CONF_FOLLOW_REDIRECTS = "follow_redirects"
CONF_REDIRECT_LIMIT = "redirect_limit"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
CONF_BUFFER_SIZE_TX = "buffer_size_tx"
CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
CONF_ON_RESPONSE = "on_response"
@ -99,7 +101,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
cv.Optional(
CONF_TIMEOUT, default="5s"
CONF_TIMEOUT, default="4.5s"
): cv.positive_time_period_milliseconds,
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
cv.only_on_esp8266, cv.boolean
@ -110,6 +112,12 @@ CONFIG_SCHEMA = cv.All(
cv.positive_not_null_time_period,
cv.positive_time_period_milliseconds,
),
cv.SplitDefault(CONF_BUFFER_SIZE_RX, esp32_idf=512): cv.All(
cv.uint16_t, cv.only_with_esp_idf
),
cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32_idf=512): cv.All(
cv.uint16_t, cv.only_with_esp_idf
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version(
@ -137,6 +145,9 @@ async def to_code(config):
if CORE.is_esp32:
if CORE.using_esp_idf:
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
config.get(CONF_VERIFY_SSL),

View file

@ -80,7 +80,7 @@ class HttpRequestComponent : public Component {
const char *useragent_{nullptr};
bool follow_redirects_;
uint16_t redirect_limit_;
uint16_t timeout_{5000};
uint16_t timeout_{4500};
uint32_t watchdog_timeout_{0};
};

View file

@ -18,6 +18,12 @@ namespace http_request {
static const char *const TAG = "http_request.idf";
void HttpRequestIDF::dump_config() {
HttpRequestComponent::dump_config();
ESP_LOGCONFIG(TAG, " Buffer Size RX: %u", this->buffer_size_rx_);
ESP_LOGCONFIG(TAG, " Buffer Size TX: %u", this->buffer_size_tx_);
}
std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::string method, std::string body,
std::list<Header> headers) {
if (!network::is_connected()) {
@ -52,6 +58,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
config.timeout_ms = this->timeout_;
config.disable_auto_redirect = !this->follow_redirects_;
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure) {
config.crt_bundle_attach = esp_crt_bundle_attach;
@ -62,6 +69,9 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
config.user_agent = this->useragent_;
}
config.buffer_size = this->buffer_size_rx_;
config.buffer_size_tx = this->buffer_size_tx_;
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
@ -76,7 +86,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
esp_http_client_set_header(client, header.name, header.value);
}
int body_len = body.length();
const int body_len = body.length();
esp_err_t err = esp_http_client_open(client, body_len);
if (err != ESP_OK) {
@ -108,18 +118,62 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
return nullptr;
}
container->content_length = esp_http_client_fetch_headers(client);
const auto status_code = esp_http_client_get_status_code(client);
container->status_code = status_code;
auto is_ok = [](int code) { return code >= HttpStatus_Ok && code < HttpStatus_MultipleChoices; };
if (status_code < 200 || status_code >= 300) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), status_code);
this->status_momentary_error("failed", 1000);
esp_http_client_cleanup(client);
return nullptr;
container->content_length = esp_http_client_fetch_headers(client);
container->status_code = esp_http_client_get_status_code(client);
if (is_ok(container->status_code)) {
container->duration_ms = millis() - start;
return container;
}
container->duration_ms = millis() - start;
return container;
if (this->follow_redirects_) {
auto is_redirect = [](int code) {
return code == HttpStatus_MovedPermanently || code == HttpStatus_Found || code == HttpStatus_SeeOther ||
code == HttpStatus_TemporaryRedirect || code == HttpStatus_PermanentRedirect;
};
auto num_redirects = this->redirect_limit_;
while (is_redirect(container->status_code) && num_redirects > 0) {
err = esp_http_client_set_redirection(client);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
this->status_momentary_error("failed", 1000);
esp_http_client_cleanup(client);
return nullptr;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char url[256]{};
if (esp_http_client_get_url(client, url, sizeof(url) - 1) == ESP_OK) {
ESP_LOGV(TAG, "redirecting to url: %s", url);
}
#endif
err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
this->status_momentary_error("failed", 1000);
esp_http_client_cleanup(client);
return nullptr;
}
container->content_length = esp_http_client_fetch_headers(client);
container->status_code = esp_http_client_get_status_code(client);
if (is_ok(container->status_code)) {
container->duration_ms = millis() - start;
return container;
}
num_redirects--;
}
if (num_redirects == 0) {
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_);
}
}
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
this->status_momentary_error("failed", 1000);
esp_http_client_cleanup(client);
return nullptr;
}
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {

View file

@ -24,8 +24,18 @@ class HttpContainerIDF : public HttpContainer {
class HttpRequestIDF : public HttpRequestComponent {
public:
void dump_config() override;
std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
std::list<Header> headers) override;
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
protected:
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
};
} // namespace http_request

View file

@ -25,6 +25,10 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
CONF_I2S_AUDIO = "i2s_audio"
CONF_I2S_AUDIO_ID = "i2s_audio_id"
CONF_I2S_MODE = "i2s_mode"
CONF_PRIMARY = "primary"
CONF_SECONDARY = "secondary"
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component)
I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent))
@ -32,6 +36,12 @@ I2SAudioOut = i2s_audio_ns.class_(
"I2SAudioOut", cg.Parented.template(I2SAudioComponent)
)
i2s_mode_t = cg.global_ns.enum("i2s_mode_t")
I2S_MODE_OPTIONS = {
CONF_PRIMARY: i2s_mode_t.I2S_MODE_MASTER, # NOLINT
CONF_SECONDARY: i2s_mode_t.I2S_MODE_SLAVE, # NOLINT
}
# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h
I2S_PORTS = {
VARIANT_ESP32: 2,

View file

@ -7,6 +7,9 @@ from esphome.components import microphone, esp32
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
from .. import (
CONF_I2S_MODE,
CONF_PRIMARY,
I2S_MODE_OPTIONS,
i2s_audio_ns,
I2SAudioComponent,
I2SAudioIn,
@ -68,6 +71,9 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
_validate_bits, cv.enum(BITS_PER_SAMPLE)
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
I2S_MODE_OPTIONS, lower=True
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -107,6 +113,7 @@ async def to_code(config):
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM]))
cg.add(var.set_i2s_mode(config[CONF_I2S_MODE]))
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))

View file

@ -46,7 +46,7 @@ void I2SAudioMicrophone::start_() {
return; // Waiting for another i2s to return lock
}
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX),
.sample_rate = this->sample_rate_,
.bits_per_sample = this->bits_per_sample_,
.channel_format = this->channel_,

View file

@ -30,6 +30,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
}
#endif
void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
@ -46,6 +48,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
bool adc_{false};
#endif
bool pdm_{false};
i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_;

View file

@ -92,7 +92,9 @@ static const uint8_t ILI9XXX_GMCTRN1 = 0xE1;
static const uint8_t ILI9XXX_CSCON = 0xF0;
static const uint8_t ILI9XXX_ADJCTL3 = 0xF7;
static const uint8_t ILI9XXX_DELAY = 0xFF; // followed by one byte of delay time in ms
static const uint8_t ILI9XXX_DELAY_FLAG = 0xFF;
// special marker for delay - command byte reprents ms, length byte is an impossible value
#define ILI9XXX_DELAY(ms) ((uint8_t) ((ms) | 0x80)), ILI9XXX_DELAY_FLAG
} // namespace ili9xxx
} // namespace esphome

View file

@ -34,8 +34,8 @@ void ILI9XXXDisplay::setup() {
ESP_LOGD(TAG, "Setting up ILI9xxx");
this->setup_pins_();
this->init_lcd(this->init_sequence_);
this->init_lcd(this->extra_init_sequence_.data());
this->init_lcd_(this->init_sequence_);
this->init_lcd_(this->extra_init_sequence_.data());
switch (this->pixel_mode_) {
case PIXEL_MODE_16:
if (this->is_18bitdisplay_) {
@ -405,42 +405,29 @@ void ILI9XXXDisplay::reset_() {
}
}
void ILI9XXXDisplay::init_lcd(const uint8_t *addr) {
void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;
while ((cmd = *addr++) != 0) {
x = *addr++;
if (cmd == ILI9XXX_DELAY) {
ESP_LOGD(TAG, "Delay %dms", x);
delay(x);
if (x == ILI9XXX_DELAY_FLAG) {
cmd &= 0x7F;
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
num_args = x & 0x7F;
ESP_LOGD(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr);
ESP_LOGV(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr);
this->send_command(cmd, addr, num_args);
addr += num_args;
if (x & 0x80) {
ESP_LOGD(TAG, "Delay 150ms");
ESP_LOGV(TAG, "Delay 150ms");
delay(150); // NOLINT
}
}
}
}
void ILI9XXXGC9A01A::init_lcd(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;
while ((cmd = *addr++) != 0) {
x = *addr++;
num_args = x & 0x7F;
this->send_command(cmd, addr, num_args);
addr += num_args;
if (x & 0x80)
delay(150); // NOLINT
}
}
// Tell the display controller where we want to draw pixels.
void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
x1 += this->offset_x_;

View file

@ -33,7 +33,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
uint8_t cmd, num_args, bits;
const uint8_t *addr = init_sequence;
while ((cmd = *addr++) != 0) {
num_args = *addr++ & 0x7F;
num_args = *addr++;
if (num_args == ILI9XXX_DELAY_FLAG)
continue;
bits = *addr;
switch (cmd) {
case ILI9XXX_MADCTL: {
@ -50,13 +52,10 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
break;
}
case ILI9XXX_DELAY:
continue; // no args to skip
default:
break;
}
addr += num_args;
addr += (num_args & 0x7F);
}
}
@ -109,7 +108,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
virtual void set_madctl();
void display_();
virtual void init_lcd(const uint8_t *addr);
void init_lcd_(const uint8_t *addr);
void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2);
void reset_();
@ -269,7 +268,6 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
class ILI9XXXGC9A01A : public ILI9XXXDisplay {
public:
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {}
void init_lcd(const uint8_t *addr) override;
};
//----------- ILI9XXX_24_TFT display --------------

View file

@ -372,9 +372,9 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = {
static const uint8_t PROGMEM INITCMD_ST7735[] = {
ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms
ILI9XXX_DELAY, 10,
ILI9XXX_DELAY(10),
ILI9XXX_SLPOUT , 0, // Exit Sleep, delay
ILI9XXX_DELAY, 10,
ILI9XXX_DELAY(10),
ILI9XXX_PIXFMT , 1, 0x05,
ILI9XXX_FRMCTR1, 3, // 4: Frame rate control, 3 args + delay:
0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D)
@ -415,9 +415,9 @@ static const uint8_t PROGMEM INITCMD_ST7735[] = {
0x00, 0x00, 0x02, 0x10,
ILI9XXX_MADCTL , 1, 0x00, // Memory Access Control, BGR
ILI9XXX_NORON , 0,
ILI9XXX_DELAY, 10,
ILI9XXX_DELAY(10),
ILI9XXX_DISPON , 0, // Display on
ILI9XXX_DELAY, 10,
ILI9XXX_DELAY(10),
00, // endo of list
};

View file

@ -1,6 +1,6 @@
import esphome.config_validation as cv
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
CONFIG_SCHEMA = cv.invalid(
"The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n"
"See https://esphome.io/components/sensor/combination.html"
)

View file

@ -0,0 +1,212 @@
import logging
import esphome.codegen as cg
from esphome.components.display import Display
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BUFFER_SIZE,
CONF_ID,
CONF_LAMBDA,
CONF_PAGES,
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .label import label_spec
from .lvcode import ConstantLiteral, LvContext
# from .menu import menu_spec
from .obj import obj_spec
from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema
from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns
from .widget import LvScrActType, Widget, add_widgets, set_obj_properties
DOMAIN = "lvgl"
DEPENDENCIES = ("display",)
AUTO_LOAD = ("key_provider",)
CODEOWNERS = ("@clydebarrow",)
LOGGER = logging.getLogger(__name__)
for widg in (
label_spec,
obj_spec,
):
WIDGET_TYPES[widg.name] = widg
lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create(
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
)
WIDGET_SCHEMA = any_widget_schema()
async def add_init_lambda(lv_component, init):
if init:
lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")])
cg.add(lv_component.add_init_lambda(lamb))
lv_defines = {} # Dict of #defines to provide as build flags
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
lv_defines[macro] = value
def as_macro(macro, value):
if value is None:
return f"#define {macro}"
return f"#define {macro} {value}"
LV_CONF_FILENAME = "lv_conf.h"
LV_CONF_H_FORMAT = """\
#pragma once
{}
"""
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in lv_defines.items()]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
def final_validation(config):
global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)
if CONF_LAMBDA in display:
raise cv.Invalid("Using lambda: in display config not compatible with LVGL")
if display[CONF_AUTO_CLEAR_ENABLED]:
raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL"
)
buffer_frac = config[CONF_BUFFER_SIZE]
if not CORE.is_host and buffer_frac > 0.5 and "psram" not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
async def to_code(config):
cg.add_library("lvgl/lvgl", "8.4.0")
CORE.add_define("USE_LVGL")
# suppress default enabling of extra widgets
add_define("_LV_KCONFIG_PRESENT")
# Always enable - lots of things use it.
add_define("LV_DRAW_COMPLEX", "1")
add_define("LV_TICK_CUSTOM", "1")
add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())")
add_define("LV_MEM_CUSTOM", "1")
add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc")
add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free")
add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc")
add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
add_define("LV_LOG_LEVEL", f"LV_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}")
add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH])
for font in helpers.lv_fonts_used:
add_define(f"LV_FONT_{font.upper()}")
if config[df.CONF_COLOR_DEPTH] == 16:
add_define(
"LV_COLOR_16_SWAP",
"1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0",
)
add_define(
"LV_COLOR_CHROMA_KEY",
await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]),
)
CORE.add_build_flag("-Isrc")
cg.add_global(lvgl_ns.using)
lv_component = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(lv_component, config)
Widget.create(config[CONF_ID], lv_component, WIDGET_TYPES[df.CONF_OBJ], config)
for display in config[df.CONF_DISPLAYS]:
cg.add(lv_component.add_display(await cg.get_variable(display)))
frac = config[CONF_BUFFER_SIZE]
if frac >= 0.75:
frac = 1
elif frac >= 0.375:
frac = 2
elif frac > 0.19:
frac = 4
else:
frac = 8
cg.add(lv_component.set_buffer_frac(int(frac)))
cg.add(lv_component.set_full_refresh(config[df.CONF_FULL_REFRESH]))
for font in helpers.esphome_fonts_used:
await cg.get_variable(font)
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
default_font = config[df.CONF_DEFAULT_FONT]
if default_font not in helpers.lv_fonts_used:
add_define(
"LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})"
)
globfont_id = ID(
df.DEFAULT_ESPHOME_FONT,
True,
type=lv_font_t.operator("ptr").operator("const"),
)
cg.new_variable(globfont_id, MockObj(default_font))
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:
add_define("LV_FONT_DEFAULT", default_font)
with LvContext():
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
Widget.set_completed()
await add_init_lambda(lv_component, LvContext.get_code())
for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}")
for use in helpers.lv_uses:
add_define(f"LV_USE_{use.upper()}")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
CORE.add_build_flag("-DLV_CONF_H=1")
CORE.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"')
def display_schema(config):
value = cv.ensure_list(cv.use_id(Display))(config)
return value or [cv.use_id(Display)(config)]
FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = (
cv.polling_component_schema("1s")
.extend(obj_schema("obj"))
.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
cv.GenerateID(df.CONF_DISPLAYS): display_schema,
cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16),
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LOG_LEVELS, upper=True
),
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian"
),
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
}
)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))

View file

@ -0,0 +1,487 @@
"""
This is the base of the import tree for LVGL. It contains constant definitions used elsewhere.
Constants already defined in esphome.const are not duplicated here and must be imported where used.
"""
from esphome import codegen as cg, config_validation as cv
from esphome.core import ID, Lambda
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from .lvcode import ConstantLiteral
class LValidator:
"""
A validator for a particular type used in LVGL. Usable in configs as a validator, also
has `process()` to convert a value during code generation
"""
def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None):
self.validator = validator
self.rtype = rtype
self.idtype = idtype
self.idexpr = idexpr
self.retmapper = retmapper
def __call__(self, value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if self.idtype is not None and isinstance(value, ID):
return cv.use_id(self.idtype)(value)
return self.validator(value)
async def process(self, value, args=()):
if value is None:
return None
if isinstance(value, Lambda):
return cg.RawExpression(
f"{await cg.process_lambda(value, args, return_type=self.rtype)}()"
)
if self.idtype is not None and isinstance(value, ID):
return cg.RawExpression(f"{value}->{self.idexpr}")
if self.retmapper is not None:
return self.retmapper(value)
return cg.safe_exp(value)
class LvConstant(LValidator):
"""
Allow one of a list of choices, mapped to upper case, and prepend the choice with the prefix.
It's also permitted to include the prefix in the value
The property `one_of` has the single case validator, and `several_of` allows a list of constants.
"""
def __init__(self, prefix: str, *choices):
self.prefix = prefix
self.choices = choices
prefixed_choices = [prefix + v for v in choices]
prefixed_validator = cv.one_of(*prefixed_choices, upper=True)
@schema_extractor("one_of")
def validator(value):
if value == SCHEMA_EXTRACT:
return self.choices
if isinstance(value, str) and value.startswith(self.prefix):
return prefixed_validator(value)
return self.prefix + cv.one_of(*choices, upper=True)(value)
super().__init__(validator, rtype=uint32)
self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
self.several_of = LValidator(
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
)
def mapper(self, value, args=()):
if isinstance(value, list):
value = "|".join(value)
return ConstantLiteral(value)
def extend(self, *choices):
"""
Extend an LVCconstant with additional choices.
:param choices: The extra choices
:return: A new LVConstant instance
"""
return LvConstant(self.prefix, *(self.choices + choices))
# Widgets
CONF_LABEL = "label"
# Parts
CONF_MAIN = "main"
CONF_SCROLLBAR = "scrollbar"
CONF_INDICATOR = "indicator"
CONF_KNOB = "knob"
CONF_SELECTED = "selected"
CONF_ITEMS = "items"
CONF_TICKS = "ticks"
CONF_TICK_STYLE = "tick_style"
CONF_CURSOR = "cursor"
CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder"
LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
"dejavu_16_persian_hebrew",
"simsun_16_cjk",
"unscii_8",
"unscii_16",
]
LV_EVENT = {
"PRESS": "PRESSED",
"SHORT_CLICK": "SHORT_CLICKED",
"LONG_PRESS": "LONG_PRESSED",
"LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT",
"CLICK": "CLICKED",
"RELEASE": "RELEASED",
"SCROLL_BEGIN": "SCROLL_BEGIN",
"SCROLL_END": "SCROLL_END",
"SCROLL": "SCROLL",
"FOCUS": "FOCUSED",
"DEFOCUS": "DEFOCUSED",
"READY": "READY",
"CANCEL": "CANCEL",
}
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT)
LV_ANIM = LvConstant(
"LV_SCR_LOAD_ANIM_",
"NONE",
"OVER_LEFT",
"OVER_RIGHT",
"OVER_TOP",
"OVER_BOTTOM",
"MOVE_LEFT",
"MOVE_RIGHT",
"MOVE_TOP",
"MOVE_BOTTOM",
"FADE_IN",
"FADE_OUT",
"OUT_LEFT",
"OUT_RIGHT",
"OUT_TOP",
"OUT_BOTTOM",
)
LOG_LEVELS = (
"TRACE",
"INFO",
"WARN",
"ERROR",
"USER",
"NONE",
)
LV_LONG_MODES = LvConstant(
"LV_LABEL_LONG_",
"WRAP",
"DOT",
"SCROLL",
"SCROLL_CIRCULAR",
"CLIP",
)
STATES = (
"default",
"checked",
"focused",
"focus_key",
"edited",
"hovered",
"pressed",
"scrolled",
"disabled",
"user_1",
"user_2",
"user_3",
"user_4",
)
PARTS = (
CONF_MAIN,
CONF_SCROLLBAR,
CONF_INDICATOR,
CONF_KNOB,
CONF_SELECTED,
CONF_ITEMS,
CONF_TICKS,
CONF_CURSOR,
CONF_TEXTAREA_PLACEHOLDER,
)
KEYBOARD_MODES = LvConstant(
"LV_KEYBOARD_MODE_",
"TEXT_LOWER",
"TEXT_UPPER",
"SPECIAL",
"NUMBER",
)
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP")
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
CHILD_ALIGNMENTS = LvConstant(
"LV_ALIGN_",
"TOP_LEFT",
"TOP_MID",
"TOP_RIGHT",
"LEFT_MID",
"CENTER",
"RIGHT_MID",
"BOTTOM_LEFT",
"BOTTOM_MID",
"BOTTOM_RIGHT",
)
SIBLING_ALIGNMENTS = LvConstant(
"LV_ALIGN_",
"OUT_LEFT_TOP",
"OUT_TOP_LEFT",
"OUT_TOP_MID",
"OUT_TOP_RIGHT",
"OUT_RIGHT_TOP",
"OUT_LEFT_MID",
"OUT_RIGHT_MID",
"OUT_LEFT_BOTTOM",
"OUT_BOTTOM_LEFT",
"OUT_BOTTOM_MID",
"OUT_BOTTOM_RIGHT",
"OUT_RIGHT_BOTTOM",
)
ALIGN_ALIGNMENTS = CHILD_ALIGNMENTS.extend(*SIBLING_ALIGNMENTS.choices)
FLEX_FLOWS = LvConstant(
"LV_FLEX_FLOW_",
"ROW",
"COLUMN",
"ROW_WRAP",
"COLUMN_WRAP",
"ROW_REVERSE",
"COLUMN_REVERSE",
"ROW_WRAP_REVERSE",
"COLUMN_WRAP_REVERSE",
)
OBJ_FLAGS = (
"hidden",
"clickable",
"click_focusable",
"checkable",
"scrollable",
"scroll_elastic",
"scroll_momentum",
"scroll_one",
"scroll_chain_hor",
"scroll_chain_ver",
"scroll_chain",
"scroll_on_focus",
"scroll_with_arrow",
"snappable",
"press_lock",
"event_bubble",
"gesture_bubble",
"adv_hittest",
"ignore_layout",
"floating",
"overflow_visible",
"layout_1",
"layout_2",
"widget_1",
"widget_2",
"user_1",
"user_2",
"user_3",
"user_4",
)
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BTNMATRIX_CTRLS = (
"HIDDEN",
"NO_REPEAT",
"DISABLED",
"CHECKABLE",
"CHECKED",
"CLICK_TRIG",
"POPOVER",
"RECOLOR",
"CUSTOM_1",
"CUSTOM_2",
)
LV_BASE_ALIGNMENTS = (
"START",
"CENTER",
"END",
)
LV_CELL_ALIGNMENTS = LvConstant(
"LV_GRID_ALIGN_",
*LV_BASE_ALIGNMENTS,
)
LV_GRID_ALIGNMENTS = LV_CELL_ALIGNMENTS.extend(
"STRETCH",
"SPACE_EVENLY",
"SPACE_AROUND",
"SPACE_BETWEEN",
)
LV_FLEX_ALIGNMENTS = LvConstant(
"LV_FLEX_ALIGN_",
*LV_BASE_ALIGNMENTS,
"SPACE_EVENLY",
"SPACE_AROUND",
"SPACE_BETWEEN",
)
LV_MENU_MODES = LvConstant(
"LV_MENU_HEADER_",
"TOP_FIXED",
"TOP_UNFIXED",
"BOTTOM_FIXED",
)
LV_CHART_TYPES = (
"NONE",
"LINE",
"BAR",
"SCATTER",
)
LV_CHART_AXES = (
"PRIMARY_Y",
"SECONDARY_Y",
"PRIMARY_X",
"SECONDARY_X",
)
CONF_ACCEPTED_CHARS = "accepted_chars"
CONF_ADJUSTABLE = "adjustable"
CONF_ALIGN = "align"
CONF_ALIGN_TO = "align_to"
CONF_ANGLE_RANGE = "angle_range"
CONF_ANIMATED = "animated"
CONF_ANIMATION = "animation"
CONF_ANTIALIAS = "antialias"
CONF_ARC_LENGTH = "arc_length"
CONF_AUTO_START = "auto_start"
CONF_BACKGROUND_STYLE = "background_style"
CONF_DECIMAL_PLACES = "decimal_places"
CONF_COLUMN = "column"
CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_BODY = "body"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLOR_END = "color_end"
CONF_COLOR_START = "color_start"
CONF_CONTROL = "control"
CONF_DEFAULT = "default"
CONF_DEFAULT_FONT = "default_font"
CONF_DIR = "dir"
CONF_DISPLAYS = "displays"
CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value"
CONF_ENTER_BUTTON = "enter_button"
CONF_ENTRIES = "entries"
CONF_FLAGS = "flags"
CONF_FLEX_FLOW = "flex_flow"
CONF_FLEX_ALIGN_MAIN = "flex_align_main"
CONF_FLEX_ALIGN_CROSS = "flex_align_cross"
CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow"
CONF_FULL_REFRESH = "full_refresh"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span"
CONF_GRID_CELL_COLUMN_SPAN = "grid_cell_column_span"
CONF_GRID_CELL_X_ALIGN = "grid_cell_x_align"
CONF_GRID_CELL_Y_ALIGN = "grid_cell_y_align"
CONF_GRID_COLUMN_ALIGN = "grid_column_align"
CONF_GRID_COLUMNS = "grid_columns"
CONF_GRID_ROW_ALIGN = "grid_row_align"
CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home"
CONF_INDICATORS = "indicators"
CONF_KEY_CODE = "key_code"
CONF_LABEL_GAP = "label_gap"
CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button"
CONF_LINE_WIDTH = "line_width"
CONF_LOG_LEVEL = "log_level"
CONF_LONG_PRESS_TIME = "long_press_time"
CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode"
CONF_MAJOR = "major"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next"
CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode"
CONF_PIVOT_X = "pivot_x"
CONF_PIVOT_Y = "pivot_y"
CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_REPEAT_COUNT = "repeat_count"
CONF_R_MOD = "r_mod"
CONF_RECOLOR = "recolor"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROWS = "rows"
CONF_SCALES = "scales"
CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index"
CONF_SHOW_SNOW = "show_snow"
CONF_SPIN_TIME = "spin_time"
CONF_SRC = "src"
CONF_START_ANGLE = "start_angle"
CONF_START_VALUE = "start_value"
CONF_STATES = "states"
CONF_STRIDE = "stride"
CONF_STYLE = "style"
CONF_STYLE_ID = "style_id"
CONF_SKIP = "skip"
CONF_SYMBOL = "symbol"
CONF_TAB_ID = "tab_id"
CONF_TABS = "tabs"
CONF_TEXT = "text"
CONF_TILE = "tile"
CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles"
CONF_TITLE = "title"
CONF_TOP_LAYER = "top_layer"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_VISIBLE_ROW_COUNT = "visible_row_count"
CONF_WIDGET = "widget"
CONF_WIDGETS = "widgets"
CONF_X = "x"
CONF_Y = "y"
CONF_ZOOM = "zoom"
# Keypad keys
LV_KEYS = LvConstant(
"LV_KEY_",
"UP",
"DOWN",
"RIGHT",
"LEFT",
"ESC",
"DEL",
"BACKSPACE",
"ENTER",
"NEXT",
"PREV",
"HOME",
"END",
)
# list of widgets and the parts allowed
WIDGET_PARTS = {
CONF_LABEL: (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
CONF_OBJ: (CONF_MAIN,),
}
DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""):
return "|".join(f"(int){prefix}{e.upper()}" for e in enums)

View file

@ -0,0 +1,76 @@
#include "lvgl_esphome.h"
#ifdef USE_LVGL_FONT
namespace esphome {
namespace lvgl {
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return nullptr;
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
return gd->data;
}
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return false;
dsc->adv_w = gd->offset_x + gd->width;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->bpp;
return true;
}
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
this->bpp = esp_font->get_bpp();
this->lv_font_.dsc = this;
this->lv_font_.line_height = this->height = esp_font->get_height();
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
this->lv_font_.underline_position = -1;
this->lv_font_.underline_thickness = 1;
}
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
if (unicode_letter == last_letter_)
return this->last_data_;
uint8_t unicode[5];
memset(unicode, 0, sizeof unicode);
if (unicode_letter > 0xFFFF) {
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[3] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7FF) {
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[2] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7F) {
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
unicode[1] = 0x80 + (unicode_letter & 0x3F);
} else {
unicode[0] = unicode_letter;
}
int match_length;
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
if (glyph_n < 0)
return nullptr;
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
this->last_letter_ = unicode_letter;
return this->last_data_;
}
} // namespace lvgl
} // namespace esphome
#endif // USES_LVGL_FONT

View file

@ -0,0 +1,70 @@
import re
from esphome import config_validation as cv
from esphome.config import Config
from esphome.const import CONF_ARGS, CONF_FORMAT
from esphome.core import CORE, ID
from esphome.yaml_util import ESPHomeDataBase
lv_uses = {
"USER_DATA",
"LOG",
"STYLE",
"FONT_PLACEHOLDER",
"THEME_DEFAULT",
}
def add_lv_use(*names):
for name in names:
lv_uses.add(name)
lv_fonts_used = set()
esphome_fonts_used = set()
REQUIRED_COMPONENTS = {}
lvgl_components_required = set()
def validate_printf(value):
cfmt = r"""
( # start of capture group 1
% # literal "%"
(?:[-+0 #]{0,5}) # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:h|l|ll|w|I|I32|I64)? # size
[cCdiouxXeEfgGaAnpsSZ] # type
)
""" # noqa
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X)
if len(matches) != len(value[CONF_ARGS]):
raise cv.Invalid(
f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!"
)
return value
def get_line_marks(value) -> list:
"""
If possible, return a preprocessor directive to identify the line number where the given id was defined.
:param id: The id in question
:return: A list containing zero or more line directives
"""
path = None
if isinstance(value, ESPHomeDataBase):
path = value.esp_range
elif isinstance(value, ID) and isinstance(CORE.config, Config):
path = CORE.config.get_path_for_id(value)[:-1]
path = CORE.config.get_deepest_document_range_for_path(path)
if path is None:
return []
return [path.start_mark.as_line_directive]
def requires_component(comp):
def validator(value):
lvgl_components_required.add(comp)
return cv.requires_component(comp)(value)
return validator

View file

@ -0,0 +1,34 @@
import esphome.config_validation as cv
from .defines import CONF_LABEL, CONF_LONG_MODE, CONF_RECOLOR, CONF_TEXT, LV_LONG_MODES
from .lv_validation import lv_bool, lv_text
from .schemas import TEXT_SCHEMA
from .types import lv_label_t
from .widget import Widget, WidgetType
class LabelType(WidgetType):
def __init__(self):
super().__init__(
CONF_LABEL,
TEXT_SCHEMA.extend(
{
cv.Optional(CONF_RECOLOR): lv_bool,
cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of,
}
),
)
@property
def w_type(self):
return lv_label_t
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
w.set_property(CONF_TEXT, await lv_text.process(value))
w.set_property(CONF_LONG_MODE, config)
w.set_property(CONF_RECOLOR, config)
label_spec = LabelType()

View file

@ -0,0 +1,170 @@
import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct
from esphome.components.font import Font
from esphome.components.sensor import Sensor
from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT
from esphome.core import HexInt
from esphome.cpp_generator import MockObj
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from . import types as ty
from .defines import LV_FONTS, LValidator, LvConstant
from .helpers import (
esphome_fonts_used,
lv_fonts_used,
lvgl_components_required,
requires_component,
)
from .lvcode import ConstantLiteral, lv_expr
from .types import lv_font_t
@schema_extractor("one_of")
def color(value):
if value == SCHEMA_EXTRACT:
return ["hex color value", "color ID"]
if isinstance(value, int):
return value
return cv.use_id(ColorStruct)(value)
def color_retmapper(value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if isinstance(value, int):
hexval = HexInt(value)
return lv_expr.color_hex(hexval)
# Must be an id
lvgl_components_required.add(CONF_COLOR)
return lv_expr.color_from(MockObj(value))
def pixels_or_percent(value):
"""A length in one axis - either a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT:
return ["pixels", "..%"]
if isinstance(value, int):
return str(cv.int_(value))
# Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})"
def zoom(value):
value = cv.float_range(0.1, 10.0)(value)
return int(value * 256)
def angle(value):
"""
Validation for an angle in degrees, converted to an integer representing 0.1deg units
:param value: The input in the range 0..360
:return: An angle in 1/10 degree units.
"""
return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10)
@schema_extractor("one_of")
def size(value):
"""A size in one axis - one of "size_content", a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT:
return ["size_content", "pixels", "..%"]
if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2])
if isinstance(value, str) and not value.endswith("%"):
if value.upper() == "SIZE_CONTENT":
return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a pixel position or a percentage")
if isinstance(value, int):
return str(cv.int_(value))
# Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})"
@schema_extractor("one_of")
def opacity(value):
consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
if value == SCHEMA_EXTRACT:
return consts.choices
value = cv.Any(cv.percentage, consts.one_of)(value)
if isinstance(value, float):
return int(value * 255)
return value
def stop_value(value):
return cv.int_range(0, 255)(value)
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()")
def lvms_validator_(value):
if value == "never":
value = "2147483647ms"
return cv.positive_time_period_milliseconds(value)
lv_milliseconds = LValidator(
lvms_validator_,
cg.int32,
retmapper=lambda x: x.total_milliseconds,
)
class TextValidator(LValidator):
def __init__(self):
super().__init__(
cv.string,
cg.const_char_ptr,
TextSensor,
"get_state().c_str()",
lambda s: cg.safe_exp(f"{s}"),
)
def __call__(self, value):
if isinstance(value, dict):
return value
return super().__call__(value)
async def process(self, value, args=()):
if isinstance(value, dict):
args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args))
format_str = cpp_string_escape(value[CONF_FORMAT])
return f"str_sprintf({format_str}, {arg_expr}).c_str()"
return await super().process(value, args)
lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()")
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()")
class LvFont(LValidator):
def __init__(self):
def lv_builtin_font(value):
fontval = cv.one_of(*LV_FONTS, lower=True)(value)
lv_fonts_used.add(fontval)
return "&lv_font_" + fontval
def validator(value):
if value == SCHEMA_EXTRACT:
return LV_FONTS
if isinstance(value, str) and value.lower() in LV_FONTS:
return lv_builtin_font(value)
fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval)
return requires_component("font")(f"{fontval}_engine->get_lv_font()")
super().__init__(validator, lv_font_t)
async def process(self, value, args=()):
return ConstantLiteral(value)
lv_font = LvFont()

View file

@ -0,0 +1,237 @@
import abc
import logging
from typing import Union
from esphome import codegen as cg
from esphome.core import ID, Lambda
from esphome.cpp_generator import (
AssignmentExpression,
CallExpression,
Expression,
LambdaExpression,
Literal,
MockObj,
RawExpression,
RawStatement,
SafeExpType,
Statement,
VariableDeclarationExpression,
statement,
)
from .helpers import get_line_marks
_LOGGER = logging.getLogger(__name__)
class CodeContext(abc.ABC):
"""
A class providing a context for code generation. Generated code will be added to the
current context. A new context will stack on the current context, and restore it
when done. Used with the `with` statement.
"""
code_context = None
@abc.abstractmethod
def add(self, expression: Union[Expression, Statement]):
pass
@staticmethod
def append(expression: Union[Expression, Statement]):
if CodeContext.code_context is not None:
CodeContext.code_context.add(expression)
return expression
def __init__(self):
self.previous: Union[CodeContext | None] = None
def __enter__(self):
self.previous = CodeContext.code_context
CodeContext.code_context = self
def __exit__(self, *args):
CodeContext.code_context = self.previous
class MainContext(CodeContext):
"""
Code generation into the main() function
"""
def add(self, expression: Union[Expression, Statement]):
return cg.add(expression)
class LvContext(CodeContext):
"""
Code generation into the LVGL initialisation code (called in `setup()`)
"""
lv_init_code: list["Statement"] = []
@staticmethod
def lv_add(expression: Union[Expression, Statement]):
if isinstance(expression, Expression):
expression = statement(expression)
if not isinstance(expression, Statement):
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
LvContext.lv_init_code.append(expression)
_LOGGER.debug("LV Adding: %s", expression)
return expression
@staticmethod
def get_code():
code = []
for exp in LvContext.lv_init_code:
text = str(statement(exp))
text = text.rstrip()
code.append(text)
return "\n".join(code) + "\n\n"
def add(self, expression: Union[Expression, Statement]):
return LvContext.lv_add(expression)
def set_style(self, prop):
return MockObj("lv_set_style_{prop}", "")
class LambdaContext(CodeContext):
"""
A context that will accumlate code for use in a lambda.
"""
def __init__(
self,
parameters: list[tuple[SafeExpType, str]],
return_type: SafeExpType = None,
):
super().__init__()
self.code_list: list[Statement] = []
self.parameters = parameters
self.return_type = return_type
def add(self, expression: Union[Expression, Statement]):
self.code_list.append(expression)
return expression
async def code(self) -> LambdaExpression:
code_text = []
for exp in self.code_list:
text = str(statement(exp))
text = text.rstrip()
code_text.append(text)
return await cg.process_lambda(
Lambda("\n".join(code_text) + "\n\n"),
self.parameters,
return_type=self.return_type,
)
class LocalVariable(MockObj):
"""
Create a local variable and enclose the code using it within a block.
"""
def __init__(self, name, type, modifier=None, rhs=None):
base = ID(name, True, type)
super().__init__(base, "")
self.modifier = modifier
self.rhs = rhs
def __enter__(self):
CodeContext.append(RawStatement("{"))
CodeContext.append(
VariableDeclarationExpression(self.base.type, self.modifier, self.base.id)
)
if self.rhs is not None:
CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs))
return self.base
def __exit__(self, *args):
CodeContext.append(RawStatement("}"))
class MockLv:
"""
A mock object that can be used to generate LVGL calls.
"""
def __init__(self, base):
self.base = base
def __getattr__(self, attr: str) -> "MockLv":
return MockLv(f"{self.base}{attr}")
def append(self, expression):
CodeContext.append(expression)
def __call__(self, *args: SafeExpType) -> "MockObj":
call = CallExpression(self.base, *args)
result = MockObj(call, "")
self.append(result)
return result
def __str__(self):
return str(self.base)
def __repr__(self):
return f"MockLv<{str(self.base)}>"
def call(self, prop, *args):
call = CallExpression(RawExpression(f"{self.base}{prop}"), *args)
result = MockObj(call, "")
self.append(result)
return result
def cond_if(self, expression: Expression):
CodeContext.append(RawExpression(f"if({expression}) {{"))
def cond_else(self):
CodeContext.append(RawExpression("} else {"))
def cond_endif(self):
CodeContext.append(RawExpression("}"))
class LvExpr(MockLv):
def __getattr__(self, attr: str) -> "MockLv":
return LvExpr(f"{self.base}{attr}")
def append(self, expression):
pass
# Top level mock for generic lv_ calls to be recorded
lv = MockLv("lv_")
# Just generate an expression
lv_expr = LvExpr("lv_")
# Mock for lv_obj_ calls
lv_obj = MockLv("lv_obj_")
# equivalent to cg.add() for the lvgl init context
def lv_add(expression: Union[Expression, Statement]):
return CodeContext.append(expression)
def add_line_marks(where):
for mark in get_line_marks(where):
lv_add(cg.RawStatement(mark))
def lv_assign(target, expression):
lv_add(RawExpression(f"{target} = {expression}"))
class ConstantLiteral(Literal):
__slots__ = ("constant",)
def __init__(self, constant: str):
super().__init__()
self.constant = constant
def __str__(self):
return self.constant

View file

@ -0,0 +1,129 @@
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
#include "lvgl_hal.h"
#include "lvgl_esphome.h"
namespace esphome {
namespace lvgl {
static const char *const TAG = "lvgl";
lv_event_code_t lv_custom_event; // NOLINT
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); }
void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
for (auto *display : this->displays_) {
display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr,
display::COLOR_ORDER_RGB, LV_BITNESS, LV_COLOR_16_SWAP);
}
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
auto now = millis();
this->draw_buffer_(area, (const uint8_t *) color_p);
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
lv_area_get_height(area), (int) (millis() - now));
lv_disp_flush_ready(disp_drv);
}
void LvglComponent::setup() {
ESP_LOGCONFIG(TAG, "LVGL Setup starts");
#if LV_USE_LOG
lv_log_register_print_cb(log_cb);
#endif
lv_init();
lv_custom_event = static_cast<lv_event_code_t>(lv_event_register_id());
auto *display = this->displays_[0];
size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto *buf = lv_custom_mem_alloc(buf_bytes);
if (buf == nullptr) {
ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes);
this->mark_failed();
this->status_set_error("Memory allocation failure");
return;
}
lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels);
lv_disp_drv_init(&this->disp_drv_);
this->disp_drv_.draw_buf = &this->draw_buf_;
this->disp_drv_.user_data = this;
this->disp_drv_.full_refresh = this->full_refresh_;
this->disp_drv_.flush_cb = static_flush_cb;
this->disp_drv_.rounder_cb = rounder_cb;
switch (display->get_rotation()) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_90;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_180;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_270;
break;
}
display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated);
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
for (const auto &v : this->init_lambdas_)
v(this->disp_);
lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete");
}
} // namespace lvgl
} // namespace esphome
size_t lv_millis(void) { return esphome::millis(); }
#if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266)
void *lv_custom_mem_alloc(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
}
return ptr;
}
void lv_custom_mem_free(void *ptr) { return free(ptr); } // NOLINT
void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT
#else
static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
void *lv_custom_mem_alloc(size_t size) {
void *ptr;
ptr = heap_caps_malloc(size, cap_bits);
if (ptr == nullptr) {
cap_bits = MALLOC_CAP_8BIT;
ptr = heap_caps_malloc(size, cap_bits);
}
if (ptr == nullptr) {
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
return nullptr;
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
#endif
return ptr;
}
void lv_custom_mem_free(void *ptr) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
#endif
if (ptr == nullptr)
return;
heap_caps_free(ptr);
}
void *lv_custom_mem_realloc(void *ptr, size_t size) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
#endif
return heap_caps_realloc(ptr, size, cap_bits);
}
#endif

View file

@ -0,0 +1,119 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_LVGL
// required for clang-tidy
#ifndef LV_CONF_H
#define LV_CONF_SKIP 1 // NOLINT
#endif
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <lvgl.h>
#include <vector>
#ifdef USE_LVGL_FONT
#include "esphome/components/font/font.h"
#endif
namespace esphome {
namespace lvgl {
extern lv_event_code_t lv_custom_event; // NOLINT
#ifdef USE_LVGL_COLOR
static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif
#if LV_COLOR_DEPTH == 16
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
#elif LV_COLOR_DEPTH == 32
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888;
#else
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
#endif
// Parent class for things that wrap an LVGL object
class LvCompound {
public:
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
lv_obj_t *obj{};
};
using LvLambdaType = std::function<void(lv_obj_t *)>;
using set_value_lambda_t = std::function<void(float)>;
using event_callback_t = void(_lv_event_t *);
using text_lambda_t = std::function<const char *()>;
#ifdef USE_LVGL_FONT
class FontEngine {
public:
FontEngine(font::Font *esp_font);
const lv_font_t *get_lv_font();
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
uint16_t baseline{};
uint16_t height{};
uint8_t bpp{};
protected:
font::Font *font_{};
uint32_t last_letter_{};
const font::GlyphData *last_data_{};
lv_font_t lv_font_{};
};
#endif // USE_LVGL_FONT
class LvglComponent : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
public:
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
}
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
static void log_cb(const char *buf) {
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
// make sure all coordinates are even
if (area->x1 & 1)
area->x1--;
if (!(area->x2 & 1))
area->x2++;
if (area->y1 & 1)
area->y1--;
if (!(area->y2 & 1))
area->y2++;
}
void loop() override { lv_timer_handler_run_in_period(5); }
void setup() override;
void update() override {}
void add_display(display::Display *display) { this->displays_.push_back(display); }
void add_init_lambda(const std::function<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); }
void dump_config() override;
void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; }
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; }
protected:
void draw_buffer_(const lv_area_t *area, const uint8_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{};
lv_disp_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{};
std::vector<std::function<void(lv_disp_t *)>> init_lambdas_;
size_t buffer_frac_{1};
bool full_refresh_{};
};
} // namespace lvgl
} // namespace esphome
#endif

View file

@ -0,0 +1,21 @@
//
// Created by Clyde Stubbs on 20/9/2023.
//
#pragma once
#ifdef __cplusplus
#define EXTERNC extern "C"
#include <cstddef>
namespace esphome {
namespace lvgl {}
} // namespace esphome
#else
#define EXTERNC extern
#include <stddef.h>
#endif
EXTERNC size_t lv_millis(void);
EXTERNC void *lv_custom_mem_alloc(size_t size);
EXTERNC void lv_custom_mem_free(void *ptr);
EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size);

View file

@ -0,0 +1,22 @@
from .defines import CONF_OBJ
from .types import lv_obj_t
from .widget import WidgetType
class ObjType(WidgetType):
"""
The base LVGL object. All other widgets inherit from this.
"""
def __init__(self):
super().__init__(CONF_OBJ, schema={}, modify_schema={})
@property
def w_type(self):
return lv_obj_t
async def to_code(self, w, config):
return []
obj_spec = ObjType()

View file

@ -0,0 +1,260 @@
from esphome import config_validation as cv
from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid, types as ty
from .defines import WIDGET_PARTS
from .helpers import (
REQUIRED_COMPONENTS,
add_lv_use,
requires_component,
validate_printf,
)
from .lv_validation import lv_font
from .types import WIDGET_TYPES, get_widget_type
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
{
cv.Optional(df.CONF_TEXT): cv.Any(
cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
cv.lambda_
),
},
),
validate_printf,
),
lvalid.lv_text,
)
}
)
# All LVGL styles and their validators
STYLE_PROPS = {
"align": df.CHILD_ALIGNMENTS.one_of,
"arc_opa": lvalid.opacity,
"arc_color": lvalid.lv_color,
"arc_rounded": lvalid.lv_bool,
"arc_width": cv.positive_int,
"anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color,
"bg_grad_color": lvalid.lv_color,
"bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of,
"bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of,
"bg_grad_stop": lvalid.stop_value,
"bg_img_opa": lvalid.opacity,
"bg_img_recolor": lvalid.lv_color,
"bg_img_recolor_opa": lvalid.opacity,
"bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity,
"border_color": lvalid.lv_color,
"border_opa": lvalid.opacity,
"border_post": lvalid.lv_bool,
"border_side": df.LvConstant(
"LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL"
).several_of,
"border_width": cv.positive_int,
"clip_corner": lvalid.lv_bool,
"height": lvalid.size,
"img_recolor": lvalid.lv_color,
"img_recolor_opa": lvalid.opacity,
"line_width": cv.positive_int,
"line_dash_width": cv.positive_int,
"line_dash_gap": cv.positive_int,
"line_rounded": lvalid.lv_bool,
"line_color": lvalid.lv_color,
"opa": lvalid.opacity,
"opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity,
"outline_pad": lvalid.size,
"outline_width": lvalid.size,
"pad_all": lvalid.size,
"pad_bottom": lvalid.size,
"pad_column": lvalid.size,
"pad_left": lvalid.size,
"pad_right": lvalid.size,
"pad_row": lvalid.size,
"pad_top": lvalid.size,
"shadow_color": lvalid.lv_color,
"shadow_ofs_x": cv.int_,
"shadow_ofs_y": cv.int_,
"shadow_opa": lvalid.opacity,
"shadow_spread": cv.int_,
"shadow_width": cv.positive_int,
"text_align": df.LvConstant(
"LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO"
).one_of,
"text_color": lvalid.lv_color,
"text_decor": df.LvConstant(
"LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH"
).several_of,
"text_font": lv_font,
"text_letter_space": cv.positive_int,
"text_line_space": cv.positive_int,
"text_opa": lvalid.opacity,
"transform_angle": lvalid.angle,
"transform_height": lvalid.pixels_or_percent,
"transform_pivot_x": lvalid.pixels_or_percent,
"transform_pivot_y": lvalid.pixels_or_percent,
"transform_zoom": lvalid.zoom,
"translate_x": lvalid.pixels_or_percent,
"translate_y": lvalid.pixels_or_percent,
"max_height": lvalid.pixels_or_percent,
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of),
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,
}
# Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of,
}
)
# Object states. Top level properties apply to MAIN
STATE_SCHEMA = cv.Schema(
{cv.Optional(state): STYLE_SCHEMA for state in df.STATES}
).extend(STYLE_SCHEMA)
# Setting object states
SET_STATE_SCHEMA = cv.Schema(
{cv.Optional(state): lvalid.lv_bool for state in df.STATES}
)
# Setting object flags
FLAG_SCHEMA = cv.Schema({cv.Optional(flag): cv.boolean for flag in df.OBJ_FLAGS})
FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
def part_schema(widget_type):
"""
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
:param widget_type: The type of widget to generate for
:return:
"""
parts = WIDGET_PARTS.get(widget_type)
if parts is None:
parts = (df.CONF_MAIN,)
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA
)
def obj_schema(widget_type: str):
"""
Create a schema for a widget type itself i.e. no allowance for children
:param widget_type:
:return:
"""
return (
part_schema(widget_type)
.extend(FLAG_SCHEMA)
.extend(ALIGN_TO_SCHEMA)
.extend(
cv.Schema(
{
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
)
)
ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t),
cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of,
cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent,
cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent,
}
)
}
# A style schema that can include text
STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
)
ALL_STYLES = {
**STYLE_PROPS,
}
def container_validator(schema, widget_type):
"""
Create a validator for a container given the widget type
:param schema: Base schema to extend
:param widget_type:
:return:
"""
def validator(value):
result = schema
if w_sch := WIDGET_TYPES[widget_type].schema:
result = result.extend(w_sch)
if value and (layout := value.get(df.CONF_LAYOUT)):
if not isinstance(layout, dict):
raise cv.Invalid("Layout value must be a dict")
ltype = layout.get(CONF_TYPE)
add_lv_use(ltype)
if value == SCHEMA_EXTRACT:
return result
return result(value)
return validator
def container_schema(widget_type, extras=None):
"""
Create a schema for a container widget of a given type. All obj properties are available, plus
the extras passed in, plus any defined for the specific widget being specified.
:param widget_type: The widget type, e.g. "img"
:param extras: Additional options to be made available, e.g. layout properties for children
:return: The schema for this type of widget.
"""
lv_type = get_widget_type(widget_type)
schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)})
if extras:
schema = schema.extend(extras)
# Delayed evaluation for recursion
return container_validator(schema, widget_type)
def widget_schema(widget_type, extras=None):
"""
Create a schema for a given widget type
:param widget_type: The name of the widget
:param extras:
:return:
"""
validator = container_schema(widget_type, extras=extras)
if required := REQUIRED_COMPONENTS.get(widget_type):
validator = cv.All(validator, requires_component(required))
return cv.Exclusive(widget_type, df.CONF_WIDGETS), validator
# All widget schemas must be defined before this is called.
def any_widget_schema(extras=None):
"""
Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
widget under the widgets: key.
:param extras: Additional schema to be applied to each generated one
:return:
"""
return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_PARTS))

View file

@ -0,0 +1,64 @@
from esphome import codegen as cg
from esphome.core import ID
from .defines import CONF_LABEL, CONF_OBJ, CONF_TEXT
uint16_t_ptr = cg.uint16.operator("ptr")
lvgl_ns = cg.esphome_ns.namespace("lvgl")
char_ptr = cg.global_ns.namespace("char").operator("ptr")
void_ptr = cg.void.operator("ptr")
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
lv_event_code_t = cg.global_ns.namespace("lv_event_code_t")
FontEngine = lvgl_ns.class_("FontEngine")
LvCompound = lvgl_ns.class_("LvCompound")
lv_font_t = cg.global_ns.class_("lv_font_t")
lv_style_t = cg.global_ns.struct("lv_style_t")
lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton")
lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t)
lv_obj_t_ptr = lv_obj_base_t.operator("ptr")
lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr")
lv_color_t = cg.global_ns.struct("lv_color_t")
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
class LvType(cg.MockObjClass):
def __init__(self, *args, **kwargs):
parens = kwargs.pop("parents", ())
super().__init__(*args, parents=parens + (lv_obj_base_t,))
self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")])
self.value = kwargs.pop("lvalue", lambda w: w.obj)
self.has_on_value = kwargs.pop("has_on_value", False)
self.value_property = None
def get_arg_type(self):
return self.args[0][0] if len(self.args) else None
class LvText(LvType):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
largs=[(cg.std_string, "text")],
lvalue=lambda w: w.get_property("text")[0],
**kwargs,
)
self.value_property = CONF_TEXT
lv_obj_t = LvType("lv_obj_t")
lv_label_t = LvText("lv_label_t")
LV_TYPES = {
CONF_LABEL: lv_label_t,
CONF_OBJ: lv_obj_t,
}
def get_widget_type(typestr: str) -> LvType:
return LV_TYPES[typestr]
CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t)

View file

@ -0,0 +1,347 @@
import sys
from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.config_validation import Invalid
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE
from esphome.core import ID, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObjClass
from .defines import (
CONF_DEFAULT,
CONF_MAIN,
CONF_SCROLLBAR_MODE,
CONF_WIDGETS,
OBJ_FLAGS,
PARTS,
STATES,
LValidator,
join_enums,
)
from .helpers import add_lv_use
from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj
from .schemas import ALL_STYLES
from .types import WIDGET_TYPES, LvCompound, lv_obj_t
EVENT_LAMB = "event_lamb__"
class WidgetType:
"""
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(self, name, schema=None, modify_schema=None):
"""
:param name: The widget name, e.g. "bar"
:param schema: The config schema for defining a widget
:param modify_schema: A schema to update the widget
"""
self.name = name
self.schema = schema or {}
if modify_schema is None:
self.modify_schema = schema
else:
self.modify_schema = modify_schema
@property
def animated(self):
return False
@property
def w_type(self):
"""
Get the type associated with this widget
:return:
"""
return lv_obj_t
def is_compound(self):
return self.w_type.inherits_from(LvCompound)
async def to_code(self, w, config: dict):
"""
Generate code for a given widget
:param w: The widget
:param config: Its configuration
:return: Generated code as a list of text lines
"""
raise NotImplementedError(f"No to_code defined for {self.name}")
def obj_creator(self, parent: MockObjClass, config: dict):
"""
Create an instance of the widget type
:param parent: The parent to which it should be attached
:param config: Its configuration
:return: Generated code as a single text line
"""
return f"lv_{self.name}_create({parent})"
def get_uses(self):
"""
Get a list of other widgets used by this one
:return:
"""
return ()
class LvScrActType(WidgetType):
"""
A "widget" representing the active screen.
"""
def __init__(self):
super().__init__("lv_scr_act()")
def obj_creator(self, parent: MockObjClass, config: dict):
return []
async def to_code(self, w, config: dict):
return []
class Widget:
"""
Represents a Widget.
"""
widgets_completed = False
@staticmethod
def set_completed():
Widget.widgets_completed = True
def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None):
self.var = var
self.type = wtype
self.config = config
self.scale = 1.0
self.step = 1.0
self.range_from = -sys.maxsize
self.range_to = sys.maxsize
self.parent = parent
@staticmethod
def create(name, var, wtype: WidgetType, config: dict = None, parent=None):
w = Widget(var, wtype, config, parent)
if name is not None:
widget_map[name] = w
return w
@property
def obj(self):
if self.type.is_compound():
return f"{self.var}->obj"
return self.var
def add_state(self, *args):
return lv_obj.add_state(self.obj, *args)
def clear_state(self, *args):
return lv_obj.clear_state(self.obj, *args)
def add_flag(self, *args):
return lv_obj.add_flag(self.obj, *args)
def clear_flag(self, *args):
return lv_obj.clear_flag(self.obj, *args)
def set_property(self, prop, value, animated: bool = None, ltype=None):
if isinstance(value, dict):
value = value.get(prop)
if value is None:
return
if isinstance(value, TimePeriod):
value = value.total_milliseconds
ltype = ltype or self.__type_base()
if animated is None or self.type.animated is not True:
lv.call(f"{ltype}_set_{prop}", self.obj, value)
else:
lv.call(
f"{ltype}_set_{prop}",
self.obj,
value,
"LV_ANIM_ON" if animated else "LV_ANIM_OFF",
)
def get_property(self, prop, ltype=None):
ltype = ltype or self.__type_base()
return f"lv_{ltype}_get_{prop}({self.obj})"
def set_style(self, prop, value, state):
if value is None:
return []
return lv.call(f"obj_set_style_{prop}", self.obj, value, state)
def __type_base(self):
wtype = self.type.w_type
base = str(wtype)
if base.startswith("Lv"):
return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower()
return f"{wtype}".removeprefix("lv_").removesuffix("_t")
def __str__(self):
return f"({self.var}, {self.type})"
# Map of widgets to their config, used for trigger generation
widget_map: dict[Any, Widget] = {}
def get_widget_generator(wid):
"""
Used to wait for a widget during code generation.
:param wid:
:return:
"""
while True:
if obj := widget_map.get(wid):
return obj
if Widget.widgets_completed:
raise Invalid(
f"Widget {wid} not found, yet all widgets should be defined by now"
)
yield
async def get_widget(wid: ID) -> Widget:
if obj := widget_map.get(wid):
return obj
return await FakeAwaitable(get_widget_generator(wid))
def collect_props(config):
"""
Collect all properties from a configuration
:param config:
:return:
"""
props = {}
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]:
if prop in config:
props[prop] = config[prop]
return props
def collect_states(config):
"""
Collect prperties for each state of a widget
:param config:
:return:
"""
states = {CONF_DEFAULT: collect_props(config)}
for state in STATES:
if state in config:
states[state] = collect_props(config[state])
return states
def collect_parts(config):
"""
Collect properties and states for all widget parts
:param config:
:return:
"""
parts = {CONF_MAIN: collect_states(config)}
for part in PARTS:
if part in config:
parts[part] = collect_states(config[part])
return parts
async def set_obj_properties(w: Widget, config):
"""Generate a list of C++ statements to apply properties to an lv_obj_t"""
parts = collect_parts(config)
for part, states in parts.items():
for state, props in states.items():
lv_state = ConstantLiteral(
f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}"
)
for prop, value in {
k: v for k, v in props.items() if k in ALL_STYLES
}.items():
if isinstance(ALL_STYLES[prop], LValidator):
value = await ALL_STYLES[prop].process(value)
w.set_style(prop, value, lv_state)
flag_clr = set()
flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT]
for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items():
if value:
flag_set.add(prop)
else:
flag_clr.add(prop)
if flag_set:
adds = join_enums(flag_set, "LV_OBJ_FLAG_")
w.add_flag(adds)
if flag_clr:
clrs = join_enums(flag_clr, "LV_OBJ_FLAG_")
w.clear_flag(clrs)
if states := config.get(CONF_STATE):
adds = set()
clears = set()
lambs = {}
for key, value in states.items():
if isinstance(value, cv.Lambda):
lambs[key] = value
elif value == "true":
adds.add(key)
else:
clears.add(key)
if adds:
adds = ConstantLiteral(join_enums(adds, "LV_STATE_"))
w.add_state(adds)
if clears:
clears = ConstantLiteral(join_enums(clears, "LV_STATE_"))
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
state = ConstantLiteral(f"LV_STATE_{key.upper}")
lv.cond_if(lamb)
w.add_state(state)
lv.cond_else()
w.clear_state(state)
lv.cond_endif()
if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE):
lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode)
async def add_widgets(parent: Widget, config: dict):
"""
Add all widgets to an object
:param parent: The enclosing obj
:param config: The configuration
:return:
"""
for w in config.get(CONF_WIDGETS) or ():
w_type, w_cnfig = next(iter(w.items()))
await widget_to_code(w_cnfig, w_type, parent.obj)
async def widget_to_code(w_cnfig, w_type, parent):
"""
Converts a Widget definition to C code.
:param w_cnfig: The widget configuration
:param w_type: The Widget type
:param parent: The parent to which the widget should be added
:return:
"""
spec: WidgetType = WIDGET_TYPES[w_type]
creator = spec.obj_creator(parent, w_cnfig)
add_lv_use(spec.name)
add_lv_use(*spec.get_uses())
wid = w_cnfig[CONF_ID]
add_line_marks(wid)
if spec.is_compound():
var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator))
else:
var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t)
lv_assign(var, creator)
widget = Widget.create(wid, var, spec, w_cnfig, parent)
await set_obj_properties(widget, w_cnfig)
await add_widgets(widget, w_cnfig)
await spec.to_code(widget, w_cnfig)

View file

@ -0,0 +1,33 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@rnauber"]
MULTI_CONF = True
CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id"
m5stack_8angle_ns = cg.esphome_ns.namespace("m5stack_8angle")
M5Stack8AngleComponent = m5stack_8angle_ns.class_(
"M5Stack8AngleComponent",
i2c.I2CDevice,
cg.Component,
)
AnalogBits = m5stack_8angle_ns.enum("AnalogBits")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(M5Stack8AngleComponent),
}
).extend(i2c.i2c_device_schema(0x43))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
M5Stack8AngleSwitchBinarySensor = m5stack_8angle_ns.class_(
"M5Stack8AngleSwitchBinarySensor",
binary_sensor.BinarySensor,
cg.PollingComponent,
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
}
)
.extend(binary_sensor.binary_sensor_schema(M5Stack8AngleSwitchBinarySensor))
.extend(cv.polling_component_schema("10s"))
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
sens = await binary_sensor.new_binary_sensor(config)
cg.add(sens.set_parent(hub))
await cg.register_component(sens, config)

View file

@ -0,0 +1,17 @@
#include "m5stack_8angle_binary_sensor.h"
namespace esphome {
namespace m5stack_8angle {
void M5Stack8AngleSwitchBinarySensor::update() {
int8_t out = this->parent_->read_switch();
if (out == -1) {
this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle.");
return;
}
this->publish_state(out != 0);
this->status_clear_warning();
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,19 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor,
public PollingComponent,
public Parented<M5Stack8AngleComponent> {
public:
void update() override;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light
from esphome.const import CONF_OUTPUT_ID
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
M5Stack8AngleLightsComponent = m5stack_8angle_ns.class_(
"M5Stack8AngleLightOutput",
light.AddressableLight,
)
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(M5Stack8AngleLightsComponent),
}
)
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
lights = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(lights, config)
await cg.register_component(lights, config)
cg.add(lights.set_parent(hub))

View file

@ -0,0 +1,45 @@
#include "m5stack_8angle_light.h"
#include "esphome/core/log.h"
namespace esphome {
namespace m5stack_8angle {
static const char *const TAG = "m5stack_8angle.light";
void M5Stack8AngleLightOutput::setup() {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
this->mark_failed();
return;
};
memset(this->buf_, 0xFF, M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
this->effect_data_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", M5STACK_8ANGLE_NUM_LEDS);
this->mark_failed();
return;
};
memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS);
}
void M5Stack8AngleLightOutput::write_state(light::LightState *state) {
for (int i = 0; i < M5STACK_8ANGLE_NUM_LEDS;
i++) { // write one LED at a time, otherwise the message will be truncated
this->parent_->write_register(M5STACK_8ANGLE_REGISTER_RGB_24B + i * M5STACK_8ANGLE_BYTES_PER_LED,
this->buf_ + i * M5STACK_8ANGLE_BYTES_PER_LED, M5STACK_8ANGLE_BYTES_PER_LED);
}
}
light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) const {
size_t pos = index * M5STACK_8ANGLE_BYTES_PER_LED;
// red, green, blue, white, effect_data, color_correction
return {this->buf_ + pos, this->buf_ + pos + 1, this->buf_ + pos + 2,
nullptr, this->effect_data_ + index, &this->correction_};
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,37 @@
#pragma once
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/light_output.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9;
static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4;
class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented<M5Stack8AngleComponent> {
public:
void setup() override;
void write_state(light::LightState *state) override;
int32_t size() const override { return M5STACK_8ANGLE_NUM_LEDS; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
};
void clear_effect_data() override { memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); };
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,74 @@
#include "m5stack_8angle.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace m5stack_8angle {
static const char *const TAG = "m5stack_8angle";
void M5Stack8AngleComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up M5STACK_8ANGLE...");
i2c::ErrorCode err;
err = this->read(nullptr, 0);
if (err != i2c::NO_ERROR) {
ESP_LOGE(TAG, "I2C error %02X...", err);
this->mark_failed();
return;
};
err = this->read_register(M5STACK_8ANGLE_REGISTER_FW_VERSION, &this->fw_version_, 1);
if (err != i2c::NO_ERROR) {
ESP_LOGE(TAG, "I2C error %02X...", err);
this->mark_failed();
return;
};
}
void M5Stack8AngleComponent::dump_config() {
ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Firmware version: %d ", this->fw_version_);
}
float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) {
int32_t raw_pos = this->read_knob_pos_raw(channel, bits);
if (raw_pos == -1) {
return NAN;
}
return (float) raw_pos / ((1 << bits) - 1);
}
int32_t M5Stack8AngleComponent::read_knob_pos_raw(uint8_t channel, AnalogBits bits) {
uint16_t knob_pos = 0;
i2c::ErrorCode err;
if (bits == BITS_8) {
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B + channel, (uint8_t *) &knob_pos, 1);
} else if (bits == BITS_12) {
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B + (channel * 2), (uint8_t *) &knob_pos, 2);
} else {
ESP_LOGE(TAG, "Invalid number of bits: %d", bits);
return -1;
}
if (err == i2c::NO_ERROR) {
return knob_pos;
} else {
return -1;
}
}
int8_t M5Stack8AngleComponent::read_switch() {
uint8_t out;
i2c::ErrorCode err = this->read_register(M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT, (uint8_t *) &out, 1);
if (err == i2c::NO_ERROR) {
return out ? 1 : 0;
} else {
return -1;
}
}
float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
namespace esphome {
namespace m5stack_8angle {
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00;
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10;
static const uint8_t M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT = 0x20;
static const uint8_t M5STACK_8ANGLE_REGISTER_RGB_24B = 0x30;
static const uint8_t M5STACK_8ANGLE_REGISTER_FW_VERSION = 0xFE;
enum AnalogBits : uint8_t {
BITS_8 = 8,
BITS_12 = 12,
};
class M5Stack8AngleComponent : public i2c::I2CDevice, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
int8_t read_switch();
protected:
uint8_t fw_version_;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,66 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_BIT_DEPTH,
CONF_CHANNEL,
CONF_RAW,
ICON_ROTATE_RIGHT,
STATE_CLASS_MEASUREMENT,
)
from .. import (
AnalogBits,
M5Stack8AngleComponent,
m5stack_8angle_ns,
CONF_M5STACK_8ANGLE_ID,
)
M5Stack8AngleKnobSensor = m5stack_8angle_ns.class_(
"M5Stack8AngleKnobSensor",
sensor.Sensor,
cg.PollingComponent,
)
BIT_DEPTHS = {
8: AnalogBits.BITS_8,
12: AnalogBits.BITS_12,
}
_validate_bits = cv.float_with_unit("bits", "bit")
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(M5Stack8AngleKnobSensor),
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8),
cv.Optional(CONF_BIT_DEPTH, default="8bit"): cv.All(
_validate_bits, cv.enum(BIT_DEPTHS)
),
cv.Optional(CONF_RAW, default=False): cv.boolean,
}
)
.extend(
sensor.sensor_schema(
M5Stack8AngleKnobSensor,
accuracy_decimals=2,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
)
)
.extend(cv.polling_component_schema("10s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID])
cg.add(var.set_channel(config[CONF_CHANNEL] - 1))
cg.add(var.set_bit_depth(BIT_DEPTHS[config[CONF_BIT_DEPTH]]))
cg.add(var.set_raw(config[CONF_RAW]))

View file

@ -0,0 +1,24 @@
#include "m5stack_8angle_sensor.h"
namespace esphome {
namespace m5stack_8angle {
void M5Stack8AngleKnobSensor::update() {
if (this->parent_ != nullptr) {
int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_);
if (raw_pos == -1) {
this->status_set_warning("Could not read knob position from M5Stack 8Angle.");
return;
}
if (this->raw_) {
this->publish_state(raw_pos);
} else {
float knob_pos = (float) raw_pos / ((1 << this->bits_) - 1);
this->publish_state(knob_pos);
}
this->status_clear_warning();
};
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
class M5Stack8AngleKnobSensor : public sensor::Sensor,
public PollingComponent,
public Parented<M5Stack8AngleComponent> {
public:
void update() override;
void set_channel(uint8_t channel) { this->channel_ = channel; };
void set_bit_depth(AnalogBits bits) { this->bits_ = bits; };
void set_raw(bool raw) { this->raw_ = raw; };
protected:
uint8_t channel_;
AnalogBits bits_;
bool raw_;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -357,7 +357,9 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(MicroWakeWord),
cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone),
cv.Required(CONF_MODELS): cv.ensure_list(MODEL_SCHEMA),
cv.Required(CONF_MODELS): cv.ensure_list(
cv.maybe_simple_value(MODEL_SCHEMA, key=CONF_MODEL)
),
cv.Optional(CONF_ON_WAKE_WORD_DETECTED): automation.validate_automation(
single=True
),

View file

@ -1,8 +1,16 @@
import binascii
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import modbus
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_LAMBDA, CONF_OFFSET
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_NAME,
CONF_LAMBDA,
CONF_OFFSET,
CONF_TRIGGER_ID,
)
from esphome.cpp_helpers import logging
from .const import (
CONF_BITMASK,
@ -12,6 +20,7 @@ from .const import (
CONF_CUSTOM_COMMAND,
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_ON_COMMAND_SENT,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
CONF_RESPONSE_SIZE,
@ -97,6 +106,10 @@ TYPE_REGISTER_MAP = {
"FP32_R": 2,
}
ModbusCommandSentTrigger = modbus_controller_ns.class_(
"ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_)
)
_LOGGER = logging.getLogger(__name__)
ModbusServerRegisterSchema = cv.Schema(
@ -120,13 +133,19 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_SERVER_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema),
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ModbusCommandSentTrigger
),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(modbus.modbus_device_schema(0x01))
)
ModbusItemBaseSchema = cv.Schema(
{
cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController),
@ -254,6 +273,11 @@ async def to_code(config):
)
)
await register_modbus_device(var, config)
for conf in config.get(CONF_ON_COMMAND_SENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(int, "function_code"), (int, "address")], conf
)
async def register_modbus_device(var, config):

View file

@ -0,0 +1,19 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/modbus_controller/modbus_controller.h"
namespace esphome {
namespace modbus_controller {
class ModbusCommandSentTrigger : public Trigger<int, int> {
public:
ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) {
a_modbuscontroller->add_on_command_sent_callback(
[this](int function_code, int address) { this->trigger(function_code, address); });
}
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -6,6 +6,7 @@ CONF_CUSTOM_COMMAND = "custom_command"
CONF_FORCE_NEW_RANGE = "force_new_range"
CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
CONF_ON_COMMAND_SENT = "on_command_sent"
CONF_RAW_ENCODE = "raw_encode"
CONF_REGISTER_COUNT = "register_count"
CONF_REGISTER_TYPE = "register_type"

View file

@ -43,7 +43,11 @@ bool ModbusController::send_next_command_() {
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
command->register_address, command->register_count);
command->send();
this->last_command_timestamp_ = millis();
this->command_sent_callback_.call((int) command->function_code, command->register_address);
// remove from queue if no handler is defined
if (!command->on_data_func) {
command_queue_.pop_front();
@ -659,5 +663,9 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
return value;
}
void ModbusController::add_on_command_sent_callback(std::function<void(int, int)> &&callback) {
this->command_sent_callback_.add(std::move(callback));
}
} // namespace modbus_controller
} // namespace esphome

View file

@ -456,6 +456,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
size_t get_command_queue_length() { return command_queue_.size(); }
/// get if the module is offline, didn't respond the last command
bool get_module_offline() { return module_offline_; }
/// Set callback for commands
void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
protected:
/// parse sensormap_ and create range of sequential addresses
@ -488,6 +490,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
bool module_offline_;
/// how many updates to skip if module is offline
uint16_t offline_skip_updates_;
CallbackManager<void(int, int)> command_sent_callback_{};
};
/** Convert vector<uint8_t> response payload to float.

View file

@ -61,6 +61,7 @@ def AUTO_LOAD():
return ["json"]
CONF_DISCOVER_IP = "discover_ip"
CONF_IDF_SEND_ASYNC = "idf_send_async"
CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check"
@ -225,6 +226,7 @@ CONFIG_SCHEMA = cv.All(
cv.boolean, cv.one_of("CLEAN", upper=True)
),
cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean,
cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean,
cv.Optional(
CONF_DISCOVERY_PREFIX, default="homeassistant"
): cv.publish_topic,
@ -328,8 +330,12 @@ async def to_code(config):
discovery_prefix = config[CONF_DISCOVERY_PREFIX]
discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR]
discovery_object_id_generator = config[CONF_DISCOVERY_OBJECT_ID_GENERATOR]
discover_ip = config[CONF_DISCOVER_IP]
if not discovery:
discovery_prefix = ""
if not discovery and not discover_ip:
cg.add(var.disable_discovery())
elif discovery == "CLEAN":
cg.add(
@ -338,6 +344,7 @@ async def to_code(config):
discovery_unique_id_generator,
discovery_object_id_generator,
discovery_retain,
discover_ip,
True,
)
)
@ -348,6 +355,7 @@ async def to_code(config):
discovery_unique_id_generator,
discovery_object_id_generator,
discovery_retain,
discover_ip,
)
)

View file

@ -66,7 +66,7 @@ void MQTTClientComponent::setup() {
}
#endif
if (this->is_discovery_enabled()) {
if (this->is_discovery_ip_enabled()) {
this->subscribe(
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
2);
@ -82,7 +82,7 @@ void MQTTClientComponent::setup() {
}
void MQTTClientComponent::send_device_info_() {
if (!this->is_connected() or !this->is_discovery_enabled()) {
if (!this->is_connected() or !this->is_discovery_ip_enabled()) {
return;
}
std::string topic = "esphome/discover/";
@ -99,6 +99,9 @@ void MQTTClientComponent::send_device_info_() {
}
}
root["name"] = App.get_name();
if (!App.get_friendly_name().empty()) {
root["friendly_name"] = App.get_friendly_name();
}
#ifdef USE_API
root["port"] = api::global_api_server->get_port();
#endif
@ -130,6 +133,10 @@ void MQTTClientComponent::send_device_info_() {
#ifdef USE_DASHBOARD_IMPORT
root["package_import_url"] = dashboard_import::get_package_import_url();
#endif
#ifdef USE_API_NOISE
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
#endif
},
2, this->discovery_info_.retain);
}
@ -140,6 +147,9 @@ void MQTTClientComponent::dump_config() {
this->ip_.str().c_str());
ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str());
ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str());
if (this->is_discovery_ip_enabled()) {
ESP_LOGCONFIG(TAG, " Discovery IP enabled");
}
if (!this->discovery_info_.prefix.empty()) {
ESP_LOGCONFIG(TAG, " Discovery prefix: '%s'", this->discovery_info_.prefix.c_str());
ESP_LOGCONFIG(TAG, " Discovery retain: %s", YESNO(this->discovery_info_.retain));
@ -581,6 +591,7 @@ void MQTTClientComponent::disable_shutdown_message() {
this->recalculate_availability_();
}
bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); }
bool MQTTClientComponent::is_discovery_ip_enabled() const { return this->discovery_info_.discover_ip; }
const Availability &MQTTClientComponent::get_availability() { return this->availability_; }
void MQTTClientComponent::recalculate_availability_() {
if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) {
@ -606,8 +617,9 @@ void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->sh
void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator,
MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain,
bool clean) {
bool discover_ip, bool clean) {
this->discovery_info_.prefix = std::move(prefix);
this->discovery_info_.discover_ip = discover_ip;
this->discovery_info_.unique_id_generator = unique_id_generator;
this->discovery_info_.object_id_generator = object_id_generator;
this->discovery_info_.retain = retain;

View file

@ -79,6 +79,7 @@ enum MQTTDiscoveryObjectIdGenerator {
struct MQTTDiscoveryInfo {
std::string prefix; ///< The Home Assistant discovery prefix. Empty means disabled.
bool retain; ///< Whether to retain discovery messages.
bool discover_ip; ///< Enable the Home Assistant device discovery.
bool clean;
MQTTDiscoveryUniqueIdGenerator unique_id_generator;
MQTTDiscoveryObjectIdGenerator object_id_generator;
@ -122,12 +123,14 @@ class MQTTClientComponent : public Component {
* @param retain Whether to retain discovery messages.
*/
void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator,
MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool clean = false);
MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool discover_ip,
bool clean = false);
/// Get Home Assistant discovery info.
const MQTTDiscoveryInfo &get_discovery_info() const;
/// Globally disable Home Assistant discovery.
void disable_discovery();
bool is_discovery_enabled() const;
bool is_discovery_ip_enabled() const;
#if ASYNC_TCP_SSL_ENABLED
/** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker.
@ -290,6 +293,7 @@ class MQTTClientComponent : public Component {
MQTTDiscoveryInfo discovery_info_{
.prefix = "homeassistant",
.retain = true,
.discover_ip = true,
.clean = false,
.unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
.object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,

View file

@ -1,14 +1,17 @@
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "ota_backend_arduino_esp32.h"
#include "ota_backend.h"
#include "ota_backend_arduino_esp32.h"
#include <Update.h>
namespace esphome {
namespace ota {
static const char *const TAG = "ota.arduino_esp32";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP32OTABackend>(); }
OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) {
@ -20,6 +23,9 @@ OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) {
uint8_t error = Update.getError();
if (error == UPDATE_ERROR_SIZE)
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error);
return OTA_RESPONSE_ERROR_UNKNOWN;
}
@ -27,16 +33,25 @@ void ArduinoESP32OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5
OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) {
size_t written = Update.write(data, len);
if (written != len) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
if (written == len) {
return OTA_RESPONSE_OK;
}
return OTA_RESPONSE_OK;
uint8_t error = Update.getError();
ESP_LOGE(TAG, "Write error: %d", error);
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
OTAResponseTypes ArduinoESP32OTABackend::end() {
if (!Update.end())
return OTA_RESPONSE_ERROR_UPDATE_END;
return OTA_RESPONSE_OK;
if (Update.end()) {
return OTA_RESPONSE_OK;
}
uint8_t error = Update.getError();
ESP_LOGE(TAG, "End error: %d", error);
return OTA_RESPONSE_ERROR_UPDATE_END;
}
void ArduinoESP32OTABackend::abort() { Update.abort(); }

View file

@ -1,16 +1,19 @@
#ifdef USE_ARDUINO
#ifdef USE_ESP8266
#include "ota_backend.h"
#include "ota_backend_arduino_esp8266.h"
#include "ota_backend.h"
#include "esphome/core/defines.h"
#include "esphome/components/esp8266/preferences.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <Updater.h>
namespace esphome {
namespace ota {
static const char *const TAG = "ota.arduino_esp8266";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP8266OTABackend>(); }
OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) {
@ -29,6 +32,9 @@ OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) {
return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
if (error == UPDATE_ERROR_SPACE)
return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error);
return OTA_RESPONSE_ERROR_UNKNOWN;
}
@ -36,16 +42,25 @@ void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(m
OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) {
size_t written = Update.write(data, len);
if (written != len) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
if (written == len) {
return OTA_RESPONSE_OK;
}
return OTA_RESPONSE_OK;
uint8_t error = Update.getError();
ESP_LOGE(TAG, "Write error: %d", error);
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
OTAResponseTypes ArduinoESP8266OTABackend::end() {
if (!Update.end())
return OTA_RESPONSE_ERROR_UPDATE_END;
return OTA_RESPONSE_OK;
if (Update.end()) {
return OTA_RESPONSE_OK;
}
uint8_t error = Update.getError();
ESP_LOGE(TAG, "End error: %d", error);
return OTA_RESPONSE_ERROR_UPDATE_END;
}
void ArduinoESP8266OTABackend::abort() {

View file

@ -1,14 +1,17 @@
#ifdef USE_LIBRETINY
#include "ota_backend.h"
#include "ota_backend_arduino_libretiny.h"
#include "ota_backend.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <Update.h>
namespace esphome {
namespace ota {
static const char *const TAG = "ota.arduino_libretiny";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoLibreTinyOTABackend>(); }
OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
@ -20,6 +23,9 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
uint8_t error = Update.getError();
if (error == UPDATE_ERROR_SIZE)
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error);
return OTA_RESPONSE_ERROR_UNKNOWN;
}
@ -27,16 +33,25 @@ void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5
OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) {
size_t written = Update.write(data, len);
if (written != len) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
if (written == len) {
return OTA_RESPONSE_OK;
}
return OTA_RESPONSE_OK;
uint8_t error = Update.getError();
ESP_LOGE(TAG, "Write error: %d", error);
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
if (!Update.end())
return OTA_RESPONSE_ERROR_UPDATE_END;
return OTA_RESPONSE_OK;
if (Update.end()) {
return OTA_RESPONSE_OK;
}
uint8_t error = Update.getError();
ESP_LOGE(TAG, "End error: %d", error);
return OTA_RESPONSE_ERROR_UPDATE_END;
}
void ArduinoLibreTinyOTABackend::abort() { Update.abort(); }

View file

@ -1,16 +1,19 @@
#ifdef USE_ARDUINO
#ifdef USE_RP2040
#include "ota_backend.h"
#include "ota_backend_arduino_rp2040.h"
#include "ota_backend.h"
#include "esphome/components/rp2040/preferences.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <Updater.h>
namespace esphome {
namespace ota {
static const char *const TAG = "ota.arduino_rp2040";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoRP2040OTABackend>(); }
OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
@ -29,6 +32,9 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
if (error == UPDATE_ERROR_SPACE)
return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error);
return OTA_RESPONSE_ERROR_UNKNOWN;
}
@ -36,16 +42,25 @@ void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md
OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) {
size_t written = Update.write(data, len);
if (written != len) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
if (written == len) {
return OTA_RESPONSE_OK;
}
return OTA_RESPONSE_OK;
uint8_t error = Update.getError();
ESP_LOGE(TAG, "Write error: %d", error);
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
OTAResponseTypes ArduinoRP2040OTABackend::end() {
if (!Update.end())
return OTA_RESPONSE_ERROR_UPDATE_END;
return OTA_RESPONSE_OK;
if (Update.end()) {
return OTA_RESPONSE_OK;
}
uint8_t error = Update.getError();
ESP_LOGE(TAG, "End error: %d", error);
return OTA_RESPONSE_ERROR_UPDATE_END;
}
void ArduinoRP2040OTABackend::abort() {

View file

@ -72,43 +72,44 @@ void PMWCS3Component::dump_config() {
LOG_SENSOR(" ", "vwc", this->vwc_sensor_);
}
void PMWCS3Component::read_data_() {
uint8_t data[8];
float e25, ec, temperature, vwc;
/////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) ////
if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) {
this->status_set_warning();
ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!");
return;
}
// NOLINT delay(100);
if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) {
ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers");
this->mark_failed();
return;
}
if (this->e25_sensor_ != nullptr) {
e25 = ((data[1] << 8) | data[0]) / 100.0;
this->e25_sensor_->publish_state(e25);
ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25);
}
if (this->ec_sensor_ != nullptr) {
ec = ((data[3] << 8) | data[2]) / 10.0;
this->ec_sensor_->publish_state(ec);
ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec);
}
if (this->temperature_sensor_ != nullptr) {
temperature = ((data[5] << 8) | data[4]) / 100.0;
this->temperature_sensor_->publish_state(temperature);
ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature);
}
if (this->vwc_sensor_ != nullptr) {
vwc = ((data[7] << 8) | data[6]) / 10.0;
this->vwc_sensor_->publish_state(vwc);
ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc);
}
// Wait for the sensor to be ready.
// 80ms empirically determined (conservative).
this->set_timeout(80, [this] {
uint8_t data[8];
float e25, ec, temperature, vwc;
if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) {
ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers");
this->mark_failed();
return;
}
if (this->e25_sensor_ != nullptr) {
e25 = ((data[1] << 8) | data[0]) / 100.0;
this->e25_sensor_->publish_state(e25);
ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25);
}
if (this->ec_sensor_ != nullptr) {
ec = ((data[3] << 8) | data[2]) / 10.0;
this->ec_sensor_->publish_state(ec);
ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec);
}
if (this->temperature_sensor_ != nullptr) {
temperature = ((data[5] << 8) | data[4]) / 100.0;
this->temperature_sensor_->publish_state(temperature);
ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature);
}
if (this->vwc_sensor_ != nullptr) {
vwc = ((data[7] << 8) | data[6]) / 10.0;
this->vwc_sensor_->publish_state(vwc);
ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc);
}
});
}
} // namespace pmwcs3

View file

@ -27,7 +27,7 @@ bool SmlFile::setup_node(SmlNode *node) {
uint8_t parse_length = length;
if (has_extended_length) {
length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f);
parse_length = length - 1;
parse_length = length;
this->pos_ += 1;
}
@ -37,7 +37,9 @@ bool SmlFile::setup_node(SmlNode *node) {
node->type = type & 0x07;
node->nodes.clear();
node->value_bytes.clear();
if (this->buffer_[this->pos_] == 0x00) { // end of message
// if the list is a has_extended_length list with e.g. 16 elements this is a 0x00 byte but not the end of message
if (!has_extended_length && this->buffer_[this->pos_] == 0x00) { // end of message
this->pos_ += 1;
} else if (is_list) { // list
this->pos_ += 1;

View file

@ -5,8 +5,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifndef __linux__
#error This HostUartComponent implementation is only for Linux
#if !(defined(__linux__) || defined(__APPLE__))
#error This HostUartComponent implementation is not supported on this host OS
#endif
#include <stdio.h>
@ -24,6 +24,9 @@
namespace {
speed_t get_baud(int baud) {
#ifdef __APPLE__
return baud;
#else
switch (baud) {
case 50:
return B50;
@ -88,6 +91,7 @@ speed_t get_baud(int baud) {
default:
return B0;
}
#endif
}
} // namespace

File diff suppressed because it is too large Load diff

View file

@ -46,29 +46,6 @@ static const char *const HEADER_CORS_REQ_PNA = "Access-Control-Request-Private-N
static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-Network";
#endif
#if USE_WEBSERVER_VERSION == 1
void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action,
const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) {
stream->print("<tr class=\"");
stream->print(klass.c_str());
if (obj->is_internal())
stream->print(" internal");
stream->print("\" id=\"");
stream->print(klass.c_str());
stream->print("-");
stream->print(obj->get_object_id().c_str());
stream->print("\"><td>");
stream->print(obj->get_name().c_str());
stream->print("</td><td></td><td>");
stream->print(action.c_str());
if (action_func) {
action_func(*stream, obj);
}
stream->print("</td>");
stream->print("</tr>");
}
#endif
UrlMatch match_url(const std::string &url, bool only_domain = false) {
UrlMatch match;
match.valid = false;
@ -102,11 +79,6 @@ WebServer::WebServer(web_server_base::WebServerBase *base)
#endif
}
#if USE_WEBSERVER_VERSION == 1
void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
#endif
@ -181,187 +153,6 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}
#elif USE_WEBSERVER_VERSION == 1
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html");
const std::string &title = App.get_name();
stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
"name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
stream->print(title.c_str());
stream->print(F("</title>"));
#ifdef USE_WEBSERVER_CSS_INCLUDE
stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
#endif
if (strlen(this->css_url_) > 0) {
stream->print(F(R"(<link rel="stylesheet" href=")"));
stream->print(this->css_url_);
stream->print(F("\">"));
}
stream->print(F("</head><body>"));
stream->print(F("<article class=\"markdown-body\"><h1>"));
stream->print(title.c_str());
stream->print(F("</h1>"));
stream->print(F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
#ifdef USE_SENSOR
for (auto *obj : App.get_sensors()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "sensor", "");
}
#endif
#ifdef USE_SWITCH
for (auto *obj : App.get_switches()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "switch", "<button>Toggle</button>");
}
#endif
#ifdef USE_BUTTON
for (auto *obj : App.get_buttons())
write_row(stream, obj, "button", "<button>Press</button>");
#endif
#ifdef USE_BINARY_SENSOR
for (auto *obj : App.get_binary_sensors()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "binary_sensor", "");
}
#endif
#ifdef USE_FAN
for (auto *obj : App.get_fans()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "fan", "<button>Toggle</button>");
}
#endif
#ifdef USE_LIGHT
for (auto *obj : App.get_lights()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "light", "<button>Toggle</button>");
}
#endif
#ifdef USE_TEXT_SENSOR
for (auto *obj : App.get_text_sensors()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "text_sensor", "");
}
#endif
#ifdef USE_COVER
for (auto *obj : App.get_covers()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
}
#endif
#ifdef USE_NUMBER
for (auto *obj : App.get_numbers()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) {
number::Number *number = (number::Number *) obj;
stream.print(R"(<input type="number" min=")");
stream.print(number->traits.get_min_value());
stream.print(R"(" max=")");
stream.print(number->traits.get_max_value());
stream.print(R"(" step=")");
stream.print(number->traits.get_step());
stream.print(R"(" value=")");
stream.print(number->state);
stream.print(R"("/>)");
});
}
}
#endif
#ifdef USE_TEXT
for (auto *obj : App.get_texts()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) {
text::Text *text = (text::Text *) obj;
auto mode = (int) text->traits.get_mode();
stream.print(R"(<input type=")");
if (mode == 2) {
stream.print(R"(password)");
} else { // default
stream.print(R"(text)");
}
stream.print(R"(" minlength=")");
stream.print(text->traits.get_min_length());
stream.print(R"(" maxlength=")");
stream.print(text->traits.get_max_length());
stream.print(R"(" pattern=")");
stream.print(text->traits.get_pattern().c_str());
stream.print(R"(" value=")");
stream.print(text->state.c_str());
stream.print(R"("/>)");
});
}
}
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) {
select::Select *select = (select::Select *) obj;
stream.print("<select>");
stream.print("<option></option>");
for (auto const &option : select->traits.get_options()) {
stream.print("<option>");
stream.print(option.c_str());
stream.print("</option>");
}
stream.print("</select>");
});
}
}
#endif
#ifdef USE_LOCK
for (auto *obj : App.get_locks()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) {
lock::Lock *lock = (lock::Lock *) obj;
stream.print("<button>Lock</button><button>Unlock</button>");
if (lock->traits.get_supports_open()) {
stream.print("<button>Open</button>");
}
});
}
}
#endif
#ifdef USE_CLIMATE
for (auto *obj : App.get_climates()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "climate", "");
}
#endif
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
if (this->allow_ota_) {
stream->print(
F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
}
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
#ifdef USE_WEBSERVER_JS_INCLUDE
if (this->js_include_ != nullptr) {
stream->print(F("<script type=\"module\" src=\"/0.js\"></script>"));
}
#endif
if (strlen(this->js_url_) > 0) {
stream->print(F("<script src=\""));
stream->print(this->js_url_);
stream->print(F("\"></script>"));
}
stream->print(F("</article></body></html>"));
request->send(stream);
}
#elif USE_WEBSERVER_VERSION >= 2
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response =

View file

@ -0,0 +1,217 @@
#include "web_server.h"
#include "esphome/core/application.h"
#if USE_WEBSERVER_VERSION == 1
namespace esphome {
namespace web_server {
void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action,
const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) {
stream->print("<tr class=\"");
stream->print(klass.c_str());
if (obj->is_internal())
stream->print(" internal");
stream->print("\" id=\"");
stream->print(klass.c_str());
stream->print("-");
stream->print(obj->get_object_id().c_str());
stream->print("\"><td>");
stream->print(obj->get_name().c_str());
stream->print("</td><td></td><td>");
stream->print(action.c_str());
if (action_func) {
action_func(*stream, obj);
}
stream->print("</td>");
stream->print("</tr>");
}
void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html");
const std::string &title = App.get_name();
stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
"name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
stream->print(title.c_str());
stream->print(F("</title>"));
#ifdef USE_WEBSERVER_CSS_INCLUDE
stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
#endif
if (strlen(this->css_url_) > 0) {
stream->print(F(R"(<link rel="stylesheet" href=")"));
stream->print(this->css_url_);
stream->print(F("\">"));
}
stream->print(F("</head><body>"));
stream->print(F("<article class=\"markdown-body\"><h1>"));
stream->print(title.c_str());
stream->print(F("</h1>"));
stream->print(F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
#ifdef USE_SENSOR
for (auto *obj : App.get_sensors()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "sensor", "");
}
#endif
#ifdef USE_SWITCH
for (auto *obj : App.get_switches()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "switch", "<button>Toggle</button>");
}
#endif
#ifdef USE_BUTTON
for (auto *obj : App.get_buttons())
write_row(stream, obj, "button", "<button>Press</button>");
#endif
#ifdef USE_BINARY_SENSOR
for (auto *obj : App.get_binary_sensors()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "binary_sensor", "");
}
#endif
#ifdef USE_FAN
for (auto *obj : App.get_fans()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "fan", "<button>Toggle</button>");
}
#endif
#ifdef USE_LIGHT
for (auto *obj : App.get_lights()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "light", "<button>Toggle</button>");
}
#endif
#ifdef USE_TEXT_SENSOR
for (auto *obj : App.get_text_sensors()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "text_sensor", "");
}
#endif
#ifdef USE_COVER
for (auto *obj : App.get_covers()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
}
#endif
#ifdef USE_NUMBER
for (auto *obj : App.get_numbers()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) {
number::Number *number = (number::Number *) obj;
stream.print(R"(<input type="number" min=")");
stream.print(number->traits.get_min_value());
stream.print(R"(" max=")");
stream.print(number->traits.get_max_value());
stream.print(R"(" step=")");
stream.print(number->traits.get_step());
stream.print(R"(" value=")");
stream.print(number->state);
stream.print(R"("/>)");
});
}
}
#endif
#ifdef USE_TEXT
for (auto *obj : App.get_texts()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) {
text::Text *text = (text::Text *) obj;
auto mode = (int) text->traits.get_mode();
stream.print(R"(<input type=")");
if (mode == 2) {
stream.print(R"(password)");
} else { // default
stream.print(R"(text)");
}
stream.print(R"(" minlength=")");
stream.print(text->traits.get_min_length());
stream.print(R"(" maxlength=")");
stream.print(text->traits.get_max_length());
stream.print(R"(" pattern=")");
stream.print(text->traits.get_pattern().c_str());
stream.print(R"(" value=")");
stream.print(text->state.c_str());
stream.print(R"("/>)");
});
}
}
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) {
select::Select *select = (select::Select *) obj;
stream.print("<select>");
stream.print("<option></option>");
for (auto const &option : select->traits.get_options()) {
stream.print("<option>");
stream.print(option.c_str());
stream.print("</option>");
}
stream.print("</select>");
});
}
}
#endif
#ifdef USE_LOCK
for (auto *obj : App.get_locks()) {
if (this->include_internal_ || !obj->is_internal()) {
write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) {
lock::Lock *lock = (lock::Lock *) obj;
stream.print("<button>Lock</button><button>Unlock</button>");
if (lock->traits.get_supports_open()) {
stream.print("<button>Open</button>");
}
});
}
}
#endif
#ifdef USE_CLIMATE
for (auto *obj : App.get_climates()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "climate", "");
}
#endif
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
if (this->allow_ota_) {
stream->print(
F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
}
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
#ifdef USE_WEBSERVER_JS_INCLUDE
if (this->js_include_ != nullptr) {
stream->print(F("<script type=\"module\" src=\"/0.js\"></script>"));
}
#endif
if (strlen(this->js_url_) > 0) {
stream->print(F("<script src=\""));
stream->print(this->js_url_);
stream->print(F("\"></script>"));
}
stream->print(F("</article></body></html>"));
request->send(stream);
}
} // namespace web_server
} // namespace esphome
#endif

View file

@ -82,8 +82,8 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
// WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and
// esp_netif_create_default_wifi_ap(), which creates the interfaces.
if (set_sta)
s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
// s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event
#ifdef USE_WIFI_AP
if (set_ap)
s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
@ -495,6 +495,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
case ESPHOME_EVENT_ID_WIFI_STA_START: {
ESP_LOGV(TAG, "Event: WiFi STA start");
// apply hostname
s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));

View file

@ -829,7 +829,6 @@ def time_of_day(value):
def date_time(date: bool, time: bool):
pattern_str = r"^" # Start of string
if date:
pattern_str += r"\d{4}-\d{1,2}-\d{1,2}"
@ -2031,6 +2030,7 @@ def require_framework_version(
esp32_arduino=None,
esp8266_arduino=None,
rp2040_arduino=None,
host=None,
max_version=False,
extra_message=None,
):
@ -2065,6 +2065,13 @@ def require_framework_version(
msg += f". {extra_message}"
raise Invalid(msg)
required = rp2040_arduino
elif CORE.is_host and framework == "host":
if host is None:
msg = "This feature is incompatible with host platform"
if extra_message:
msg += f". {extra_message}"
raise Invalid(msg)
required = host
else:
raise Invalid(
f"""

View file

@ -38,6 +38,9 @@
#define USE_LIGHT
#define USE_LOCK
#define USE_LOGGER
#define USE_LVGL
#define USE_LVGL_FONT
#define USE_LVGL_IMAGE
#define USE_MDNS
#define USE_MEDIA_PLAYER
#define USE_MQTT

View file

@ -1,17 +1,17 @@
import logging
from typing import Callable, Optional, Any, ContextManager
from types import ModuleType
import importlib
import importlib.util
import importlib.resources
import importlib.abc
import sys
from pathlib import Path
from dataclasses import dataclass
import importlib
import importlib.abc
import importlib.resources
import importlib.util
import logging
from pathlib import Path
import sys
from types import ModuleType
from typing import Any, Callable, ContextManager, Optional
from esphome.const import SOURCE_FILE_EXTENSIONS
import esphome.core.config
from esphome.core import CORE
import esphome.core.config
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@ -175,7 +175,11 @@ def _lookup_module(domain):
try:
module = importlib.import_module(f"esphome.components.{domain}")
except ImportError as e:
if "No module named" not in str(e):
if "No module named" in str(e):
_LOGGER.error(
"Unable to import component %s: %s", domain, str(e), exc_info=False
)
else:
_LOGGER.error("Unable to import component %s:", domain, exc_info=True)
return None
except Exception: # pylint: disable=broad-except

View file

@ -42,6 +42,7 @@ lib_deps =
pavlodn/HaierProtocol@0.9.31 ; haier
; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
lvgl/lvgl@8.4.0 ; lvgl
build_flags =
-DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
src_filter =

View file

@ -105,3 +105,33 @@ disable = [
[tool.pylint.FORMAT]
expected-line-ending-format = "LF"
[tool.ruff]
required-version = ">=0.5.0"
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes/autoflake
"I", # isort
"PL", # pylint
"UP", # pyupgrade
]
ignore = [
"E501", # line too long
"PLR0911", # Too many return statements ({returns} > {max_returns})
"PLR0912", # Too many branches ({branches} > {max_branches})
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
"PLR0915", # Too many statements ({statements} > {max_statements})
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
]
[tool.ruff.lint.isort]
force-sort-within-sections = true
known-first-party = [
"esphome",
]
combine-as-imports = true
split-on-trailing-comma = false

View file

@ -1,5 +1,5 @@
async_timeout==4.0.3; python_version <= "3.10"
cryptography==42.0.2
cryptography==43.0.0
voluptuous==0.14.2
PyYAML==6.0.1
paho-mqtt==1.6.1
@ -13,7 +13,7 @@ platformio==6.1.15 # When updating platformio, also update Dockerfile
esptool==4.7.0
click==8.1.7
esphome-dashboard==20240620.0
aioesphomeapi==24.3.0
aioesphomeapi==24.6.2
zeroconf==0.132.2
python-magic==0.4.27
ruamel.yaml==0.18.6 # dashboard_import

View file

@ -0,0 +1,12 @@
i2c:
- id: i2c_apds9306
scl: ${scl_pin}
sda: ${sda_pin}
sensor:
- platform: apds9306
name: "APDS9306 Light Level"
gain: 3
bit_width: 16
measurement_rate: 2000ms
update_interval: 60s

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO22
sda_pin: GPIO21
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO22
sda_pin: GPIO21
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
scl_pin: GPIO5
sda_pin: GPIO4
<<: !include common.yaml

View file

@ -3,6 +3,13 @@ remote_transmitter:
carrier_duty_percent: 50%
climate:
- platform: heatpumpir
protocol: mitsubishi_heavy_zm
horizontal_default: left
vertical_default: up
name: HeatpumpIR Climate
min_temperature: 18
max_temperature: 30
- platform: heatpumpir
protocol: daikin
horizontal_default: mleft

Some files were not shown because too many files have changed in this diff Show more