Merge branch 'dev' into add-graphical-layout-system

This commit is contained in:
Michael Davidson 2024-01-10 22:54:41 +11:00 committed by GitHub
commit 97de871a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 860 additions and 254 deletions

View File

@ -28,11 +28,20 @@ runs:
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
shell: bash
run: |
python -m venv venv
. venv/bin/activate
source venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
shell: bash
run: |
python -m venv venv
./venv/Scripts/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .

View File

@ -166,7 +166,35 @@ jobs:
pytest:
name: Run pytest
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
os:
- ubuntu-latest
- macOS-latest
- windows-latest
exclude:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.12"
os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.9"
os: windows-latest
- python-version: "3.12"
os: macOS-latest
- python-version: "3.10"
os: macOS-latest
- python-version: "3.9"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
- common
steps:
@ -175,14 +203,24 @@ jobs:
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version: ${{ matrix.python-version }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run pytest
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native tests
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --tb=native tests
pytest -vv --cov-report=xml --tb=native tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
clang-format:
name: Check clang-format

View File

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.0
rev: 23.12.1
hooks:
- id: black
args:

View File

@ -54,6 +54,8 @@ esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bmi160/* @flaviut
esphome/components/bmp3xx/* @martgras

View File

@ -1,116 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
bme280_ns = cg.esphome_ns.namespace("bme280")
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
"1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
"2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
"4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
"8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
"16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
}
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
"2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
"4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
"8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
"16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
}
BME280Component = bme280_ns.class_(
"BME280Component", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BME280Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x77))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View File

@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]

View File

@ -1,9 +1,14 @@
#include "bme280.h"
#include <cmath>
#include <cstdint>
#include "bme280_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <esphome/components/sensor/sensor.h>
#include <esphome/core/component.h>
namespace esphome {
namespace bme280 {
namespace bme280_base {
static const char *const TAG = "bme280.sensor";
@ -46,7 +51,24 @@ static const uint8_t BME280_STATUS_IM_UPDATE = 0b01;
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
static const char *oversampling_to_str(BME280Oversampling oversampling) {
const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT
switch (filter) {
case BME280_IIR_FILTER_OFF:
return "OFF";
case BME280_IIR_FILTER_2X:
return "2x";
case BME280_IIR_FILTER_4X:
return "4x";
case BME280_IIR_FILTER_8X:
return "8x";
case BME280_IIR_FILTER_16X:
return "16x";
default:
return "UNKNOWN";
}
}
const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT
switch (oversampling) {
case BME280_OVERSAMPLING_NONE:
return "None";
@ -65,23 +87,6 @@ static const char *oversampling_to_str(BME280Oversampling oversampling) {
}
}
static const char *iir_filter_to_str(BME280IIRFilter filter) {
switch (filter) {
case BME280_IIR_FILTER_OFF:
return "OFF";
case BME280_IIR_FILTER_2X:
return "2x";
case BME280_IIR_FILTER_4X:
return "4x";
case BME280_IIR_FILTER_8X:
return "8x";
case BME280_IIR_FILTER_16X:
return "16x";
default:
return "UNKNOWN";
}
}
void BME280Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up BME280...");
uint8_t chip_id = 0;
@ -112,7 +117,7 @@ void BME280Component::setup() {
// Wait until the NVM data has finished loading.
uint8_t status;
uint8_t retry = 5;
do {
do { // NOLINT
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
ESP_LOGW(TAG, "Error reading status register.");
@ -175,7 +180,6 @@ void BME280Component::setup() {
}
void BME280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BME280:");
LOG_I2C_DEVICE(this);
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with BME280 failed!");
@ -226,14 +230,14 @@ void BME280Component::update() {
return;
}
int32_t t_fine = 0;
float temperature = this->read_temperature_(data, &t_fine);
float const temperature = this->read_temperature_(data, &t_fine);
if (std::isnan(temperature)) {
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
this->status_set_warning();
return;
}
float pressure = this->read_pressure_(data, t_fine);
float humidity = this->read_humidity_(data, t_fine);
float const pressure = this->read_pressure_(data, t_fine);
float const humidity = this->read_humidity_(data, t_fine);
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
if (this->temperature_sensor_ != nullptr)
@ -257,11 +261,11 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
const int32_t t2 = this->calibration_.t2;
const int32_t t3 = this->calibration_.t3;
int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
*t_fine = var1 + var2;
float temperature = (*t_fine * 5 + 128) >> 8;
float const temperature = (*t_fine * 5 + 128) >> 8;
return temperature / 100.0f;
}
@ -303,11 +307,11 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
}
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
if (raw_adc == 0x8000)
return NAN;
int32_t adc = raw_adc;
int32_t const adc = raw_adc;
const int32_t h1 = this->calibration_.h1;
const int32_t h2 = this->calibration_.h2;
@ -325,7 +329,7 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r;
v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r;
float h = v_x1_u32r >> 12;
float const h = v_x1_u32r >> 12;
return h / 1024.0f;
}
@ -351,5 +355,5 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) {
}
int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
} // namespace bme280
} // namespace bme280_base
} // namespace esphome

View File

@ -2,10 +2,9 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bme280 {
namespace bme280_base {
/// Internal struct storing the calibration values of an BME280.
struct BME280CalibrationData {
@ -57,8 +56,8 @@ enum BME280IIRFilter {
BME280_IIR_FILTER_16X = 0b100,
};
/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor.
class BME280Component : public PollingComponent, public i2c::I2CDevice {
/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor.
class BME280Component : public PollingComponent {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
@ -91,6 +90,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
uint16_t read_u16_le_(uint8_t a_register);
int16_t read_s16_le_(uint8_t a_register);
virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
BME280CalibrationData calibration_;
BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X};
BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
} error_code_{NONE};
};
} // namespace bme280
} // namespace bme280_base
} // namespace esphome

View File

@ -0,0 +1,106 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_PERCENT,
)
bme280_ns = cg.esphome_ns.namespace("bme280_base")
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
"1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
"2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
"4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
"8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
"16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
}
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
"2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
"4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
"8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
"16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
}
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code(config, func=None):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if func is not None:
await func(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View File

@ -0,0 +1,30 @@
#include <cstddef>
#include <cstdint>
#include "bme280_i2c.h"
#include "esphome/components/i2c/i2c.h"
#include "../bme280_base/bme280_base.h"
namespace esphome {
namespace bme280_i2c {
bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
return I2CDevice::read_byte(a_register, data);
};
bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
return I2CDevice::write_byte(a_register, data);
};
bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
return I2CDevice::read_bytes(a_register, data, len);
};
bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
return I2CDevice::read_byte_16(a_register, data);
};
void BME280I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BME280Component::dump_config();
}
} // namespace bme280_i2c
} // namespace esphome

View File

@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bme280_i2c {
static const char *const TAG = "bme280_i2c.sensor";
class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
bool read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
void dump_config() override;
};
} // namespace bme280_i2c
} // namespace esphome

View File

@ -0,0 +1,19 @@
import esphome.codegen as cg
from esphome.components import i2c
from ..bme280_base.sensor import to_code as to_code_base, cv, CONFIG_SCHEMA_BASE
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["bme280_base"]
bme280_ns = cg.esphome_ns.namespace("bme280_i2c")
BME280I2CComponent = bme280_ns.class_(
"BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x77)
).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)})
async def to_code(config):
await to_code_base(config, func=i2c.register_i2c_device)

View File

@ -0,0 +1 @@
CODEOWNERS = ["@apbodrov"]

View File

@ -0,0 +1,66 @@
#include <cstdint>
#include <cstddef>
#include "bme280_spi.h"
#include <esphome/components/bme280_base/bme280_base.h>
int set_bit(uint8_t num, int position) {
int mask = 1 << position;
return num | mask;
}
int clear_bit(uint8_t num, int position) {
int mask = 1 << position;
return num & ~mask;
}
namespace esphome {
namespace bme280_spi {
void BME280SPIComponent::setup() {
this->spi_setup();
BME280Component::setup();
};
// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used
// and replaced by a read/write bit (RW = 0 for write and RW = 1 for read).
// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf
bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
this->enable();
// cause: *data = this->delegate_->transfer(tmp) doesnt work
this->delegate_->transfer(set_bit(a_register, 7));
*data = this->delegate_->transfer(0);
this->disable();
return true;
}
bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
this->enable();
this->delegate_->transfer(clear_bit(a_register, 7));
this->delegate_->transfer(data);
this->disable();
return true;
}
bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
this->enable();
this->delegate_->transfer(set_bit(a_register, 7));
this->delegate_->read_array(data, len);
this->disable();
return true;
}
bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
this->enable();
this->delegate_->transfer(set_bit(a_register, 7));
((uint8_t *) data)[1] = this->delegate_->transfer(0);
((uint8_t *) data)[0] = this->delegate_->transfer(0);
this->disable();
return true;
}
} // namespace bme280_spi
} // namespace esphome

View File

@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace bme280_spi {
class BME280SPIComponent : public esphome::bme280_base::BME280Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
void setup() override;
bool read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
};
} // namespace bme280_spi
} // namespace esphome

View File

@ -0,0 +1,24 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.bme280_base.sensor import (
to_code as to_code_base,
cv,
CONFIG_SCHEMA_BASE,
)
DEPENDENCIES = ["spi"]
AUTO_LOAD = ["bme280_base"]
bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi")
BME280SPIComponent = bme280_spi_ns.class_(
"BME280SPIComponent", cg.PollingComponent, spi.SPIDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
{cv.GenerateID(): cv.declare_id(BME280SPIComponent)}
)
async def to_code(config):
await to_code_base(config, func=spi.register_spi_device)

View File

@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5)
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 6)
# The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
@ -271,8 +271,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 0), None),
"dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
}

View File

@ -1,23 +1,26 @@
import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.const import (
CONF_ACTIVE,
CONF_DURATION,
CONF_ID,
CONF_INTERVAL,
CONF_DURATION,
CONF_TRIGGER_ID,
CONF_MAC_ADDRESS,
CONF_SERVICE_UUID,
CONF_MANUFACTURER_ID,
CONF_ON_BLE_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.components import esp32_ble
from esphome.core import CORE
from esphome.components.esp32 import add_idf_sdkconfig_option
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
@ -263,7 +266,10 @@ async def to_code(config):
# https://github.com/espressif/esp-idf/issues/2503
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6):
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
else:
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts

View File

@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema)
DEFAULT_GLYPHS = (
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
)
CONF_RAW_GLYPH_ID = "raw_glyph_id"

View File

@ -20,7 +20,9 @@ DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_PDM = "pdm"
CONF_SAMPLE_RATE = "sample_rate"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_USE_APLL = "use_apll"
I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
@ -62,9 +64,11 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All(
_validate_bits, cv.enum(BITS_PER_SAMPLE)
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
@ -105,6 +109,8 @@ async def to_code(config):
cg.add(var.set_pdm(config[CONF_PDM]))
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]))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
await microphone.register_microphone(var, config)

View File

@ -47,14 +47,14 @@ void I2SAudioMicrophone::start_() {
}
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = 16000,
.sample_rate = this->sample_rate_,
.bits_per_sample = this->bits_per_sample_,
.channel_format = this->channel_,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false,
.use_apll = this->use_apll_,
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,

View File

@ -31,7 +31,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#endif
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; }
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
protected:
void start_();
@ -45,7 +47,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#endif
bool pdm_{false};
i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_;
bool use_apll_;
HighFrequencyLoopRequester high_freq_;
};

View File

@ -37,6 +37,7 @@ class Image : public display::BaseImage {
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
int get_width() const override;
int get_height() const override;
const uint8_t *get_data_start() { return this->data_start_; }
ImageType get_type() const;
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;

View File

@ -309,7 +309,7 @@ async def component_to_code(config):
lt_options["LT_UART_SILENT_ENABLED"] = 0
lt_options["LT_UART_SILENT_ALL"] = 0
# set default UART port
if uart_port := framework.get(CONF_UART_PORT, None) is not None:
if (uart_port := framework.get(CONF_UART_PORT, None)) is not None:
lt_options["LT_UART_DEFAULT_PORT"] = uart_port
# add custom options
lt_options.update(framework[CONF_OPTIONS])

View File

@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import display, uart
from esphome.components import esp32
from esphome.const import (
CONF_ID,
CONF_LAMBDA,
@ -96,6 +97,11 @@ async def to_code(config):
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
elif CORE.is_esp32 and CORE.using_esp_idf:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True
)
elif CORE.is_esp8266 and CORE.using_arduino:
cg.add_library("ESP8266HTTPClient", None)

View File

@ -750,6 +750,50 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void filled_circle(int center_x, int center_y, int radius, Color color);
/**
* Draws a QR code in the screen
* @param x1 The top left x coordinate to start the QR code.
* @param y1 The top left y coordinate to start the QR code.
* @param content The content of the QR code (as a plain text - Nextion will generate the QR code).
* @param size The size (in pixels) for the QR code. Defaults to 200px.
* @param background_color The background color to draw with (as rgb565 integer). Defaults to 65535 (white).
* @param foreground_color The foreground color to draw with (as rgb565 integer). Defaults to 0 (black).
* @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo).
* @param border_width The border width (in pixels) for the QR code. Defaults to 8px.
*
* Example:
* ```cpp
* it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;");
* ```
*
* Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25).
*/
void qrcode(int x1, int y1, const char *content, int size = 200, uint16_t background_color = 65535,
uint16_t foreground_color = 0, int logo_pic = -1, uint8_t border_width = 8);
/**
* Draws a QR code in the screen
* @param x1 The top left x coordinate to start the QR code.
* @param y1 The top left y coordinate to start the QR code.
* @param content The content of the QR code (as a plain text - Nextion will generate the QR code).
* @param size The size (in pixels) for the QR code. Defaults to 200px.
* @param background_color The background color to draw with (as Color). Defaults to 65535 (white).
* @param foreground_color The foreground color to draw with (as Color). Defaults to 0 (black).
* @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo).
* @param border_width The border width (in pixels) for the QR code. Defaults to 8px.
*
* Example:
* ```cpp
* auto blue = Color(0, 0, 255);
* auto red = Color(255, 0, 0);
* it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;", 150, blue, red);
* ```
*
* Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25) with size of 150px in
* red on a blue background.
*/
void qrcode(int x1, int y1, const char *content, int size, Color background_color = Color(255, 255, 255),
Color foreground_color = Color(0, 0, 0), int logo_pic = -1, uint8_t border_width = 8);
/** Set the brightness of the backlight.
*
* @param brightness The brightness percentage from 0 to 1.0.

View File

@ -294,6 +294,19 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color)
display::ColorUtil::color_to_565(color));
}
void Nextion::qrcode(int x1, int y1, const char *content, int size, uint16_t background_color,
uint16_t foreground_color, int logo_pic, uint8_t border_width) {
this->add_no_result_to_queue_with_printf_("qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size,
background_color, foreground_color, logo_pic, border_width, content);
}
void Nextion::qrcode(int x1, int y1, const char *content, int size, Color background_color, Color foreground_color,
int logo_pic, uint8_t border_width) {
this->add_no_result_to_queue_with_printf_(
"qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color),
display::ColorUtil::color_to_565(foreground_color), logo_pic, border_width, content);
}
void Nextion::set_nextion_rtc_time(ESPTime time) {
this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year);
this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month);

View File

@ -24,7 +24,7 @@ int Nextion::upload_range(const std::string &url, int range_start) {
ESP_LOGVV(TAG, "url: %s", url.c_str());
uint range_size = this->tft_size_ - range_start;
ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_);
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_;
if (range_size <= 0 or range_end <= range_start) {
ESP_LOGE(TAG, "Invalid range");
@ -37,6 +37,8 @@ int Nextion::upload_range(const std::string &url, int range_start) {
esp_http_client_config_t config = {
.url = url.c_str(),
.cert_pem = nullptr,
.disable_auto_redirect = false,
.max_redirection_count = 10,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
@ -44,7 +46,7 @@ int Nextion::upload_range(const std::string &url, int range_start) {
sprintf(range_header, "bytes=%d-%d", range_start, range_end);
ESP_LOGV(TAG, "Requesting range: %s", range_header);
esp_http_client_set_header(client, "Range", range_header);
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
ESP_LOGV(TAG, "Opening http connetion");
esp_err_t err;
@ -70,13 +72,13 @@ int Nextion::upload_range(const std::string &url, int range_start) {
std::string recv_string;
if (buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for buffer");
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
} else {
ESP_LOGV(TAG, "Memory for buffer allocated successfully");
while (true) {
App.feed_wdt();
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
int read_len = esp_http_client_read(client, reinterpret_cast<char *>(buffer), 4096);
ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len);
if (read_len > 0) {
@ -145,17 +147,19 @@ bool Nextion::upload_tft() {
// Define the configuration for the HTTP client
ESP_LOGV(TAG, "Establishing connection to HTTP server");
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
esp_http_client_config_t config = {
.url = this->tft_url_.c_str(),
.cert_pem = nullptr,
.method = HTTP_METHOD_HEAD,
.timeout_ms = 15000,
.disable_auto_redirect = false,
.max_redirection_count = 10,
};
// Initialize the HTTP client with the configuration
ESP_LOGV(TAG, "Initializing HTTP client");
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
esp_http_client_handle_t http = esp_http_client_init(&config);
if (!http) {
ESP_LOGE(TAG, "Failed to initialize HTTP client.");
@ -164,7 +168,7 @@ bool Nextion::upload_tft() {
// Perform the HTTP request
ESP_LOGV(TAG, "Check if the client could connect");
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size());
esp_err_t err = esp_http_client_perform(http);
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
@ -256,7 +260,7 @@ bool Nextion::upload_end(bool successful) {
this->soft_reset();
vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT
if (successful) {
ESP_LOGD(TAG, "Restarting esphome");
ESP_LOGD(TAG, "Restarting ESPHome");
esp_restart(); // NOLINT(readability-static-accessed-through-instance)
}
return successful;

View File

@ -19,7 +19,7 @@ PylontechComponent = pylontech_ns.class_(
)
PylontechBattery = pylontech_ns.class_("PylontechBattery")
CV_NUM_BATTERIES = cv.int_range(1, 6)
CV_NUM_BATTERIES = cv.int_range(1, 16)
PYLONTECH_COMPONENT_SCHEMA = cv.Schema(
{

View File

@ -1,5 +1,6 @@
#include "pylontech.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace pylontech {
@ -34,26 +35,30 @@ void PylontechComponent::setup() {
void PylontechComponent::update() { this->write_str("pwr\n"); }
void PylontechComponent::loop() {
uint8_t data;
// pylontech sends a lot of data very suddenly
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
while (this->available() > 0) {
if (this->read_byte(&data)) {
buffer_[buffer_index_write_] += (char) data;
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
// complete line received
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
if (this->available() > 0) {
// pylontech sends a lot of data very suddenly
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
uint8_t data;
int recv = 0;
while (this->available() > 0) {
if (this->read_byte(&data)) {
buffer_[buffer_index_write_] += (char) data;
recv++;
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
// complete line received
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
}
}
}
}
// only process one line per call of loop() to not block esphome for too long
if (buffer_index_read_ != buffer_index_write_) {
this->process_line_(buffer_[buffer_index_read_]);
buffer_[buffer_index_read_].clear();
buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
ESP_LOGV(TAG, "received %d bytes", recv);
} else {
// only process one line per call of loop() to not block esphome for too long
if (buffer_index_read_ != buffer_index_write_) {
this->process_line_(buffer_[buffer_index_read_]);
buffer_[buffer_index_read_].clear();
buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
}
}
}
@ -66,10 +71,11 @@ void PylontechComponent::process_line_(std::string &buffer) {
// clang-format on
PylontechListener::LineContents l{};
const int parsed = sscanf( // NOLINT
buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %d %*s", // NOLINT
&l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT
l.curr_st, l.temp_st, &l.coulomb, &l.mostempr); // NOLINT
char mostempr_s[6];
const int parsed = sscanf( // NOLINT
buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT
&l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT
l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT
if (l.bat_num <= 0) {
ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str());
@ -79,6 +85,13 @@ void PylontechComponent::process_line_(std::string &buffer) {
ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str());
return;
}
auto mostempr_parsed = parse_number<int>(mostempr_s);
if (mostempr_parsed.has_value()) {
l.mostempr = mostempr_parsed.value();
} else {
l.mostempr = -300;
ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num);
}
for (PylontechListener *listener : this->listeners_) {
listener->on_line_read(&l);

View File

@ -59,14 +59,14 @@ TYPES: dict[str, cv.Schema] = {
device_class=DEVICE_CLASS_TEMPERATURE,
),
CONF_VOLTAGE_LOW: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
),
CONF_VOLTAGE_HIGH: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
),
CONF_COULOMB: sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,

View File

@ -227,16 +227,17 @@ optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
}
void ProntoProtocol::dump(const ProntoData &data) {
std::string first, rest;
if (data.data.size() < 230) {
first = data.data;
} else {
first = data.data.substr(0, 229);
rest = data.data.substr(230);
}
ESP_LOGI(TAG, "Received Pronto: data=%s", first.c_str());
if (!rest.empty()) {
ESP_LOGI(TAG, "%s", rest.c_str());
std::string rest;
rest = data.data;
ESP_LOGI(TAG, "Received Pronto: data=");
while (true) {
ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str());
if (rest.size() > 230) {
rest = rest.substr(230);
} else {
break;
}
}
}

View File

@ -122,7 +122,7 @@ class UARTComponent {
// @return Baud rate in bits per second.
uint32_t get_baud_rate() const { return baud_rate_; }
#ifdef USE_ESP32
#if defined(USE_ESP8266) || defined(USE_ESP32)
/**
* Load the UART settings.
* @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly.
@ -147,7 +147,7 @@ class UARTComponent {
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
virtual void load_settings(){};
#endif // USE_ESP32
#endif // USE_ESP8266 || USE_ESP32
#ifdef USE_UART_DEBUGGER
void add_debug_callback(std::function<void(UARTDirection, uint8_t)> &&callback) {

View File

@ -98,10 +98,26 @@ void ESP8266UartComponent::setup() {
}
}
void ESP8266UartComponent::load_settings(bool dump_config) {
ESP_LOGCONFIG(TAG, "Loading UART bus settings...");
if (this->hw_serial_ != nullptr) {
SerialConfig config = static_cast<SerialConfig>(get_config());
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
} else {
this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_,
this->parity_, this->rx_buffer_size_);
}
if (dump_config) {
ESP_LOGCONFIG(TAG, "UART bus was reloaded.");
this->dump_config();
}
}
void ESP8266UartComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus:");
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
LOG_PIN(" TX Pin: ", this->tx_pin_);
LOG_PIN(" RX Pin: ", this->rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT
}

View File

@ -63,6 +63,21 @@ class ESP8266UartComponent : public UARTComponent, public Component {
uint32_t get_config();
/**
* Load the UART with the current settings.
* @param dump_config (Optional, default `true`): True for displaying new settings or
* false to change it quitely
*
* Example:
* ```cpp
* id(uart1).load_settings();
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
void load_settings(bool dump_config) override;
void load_settings() override { this->load_settings(true); }
protected:
void check_logger_conflict() override;

View File

@ -4,5 +4,7 @@ EVENT_ENTRY_ADDED = "entry_added"
EVENT_ENTRY_REMOVED = "entry_removed"
EVENT_ENTRY_UPDATED = "entry_updated"
EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
MAX_EXECUTOR_WORKERS = 48
SENTINEL = object()

View File

@ -8,6 +8,7 @@ from functools import partial
from typing import TYPE_CHECKING, Any, Callable
from ..zeroconf import DiscoveredImport
from .dns import DNSCache
from .entries import DashboardEntries
from .settings import DashboardSettings
@ -69,6 +70,7 @@ class ESPHomeDashboard:
"mqtt_ping_request",
"mdns_status",
"settings",
"dns_cache",
)
def __init__(self) -> None:
@ -81,7 +83,8 @@ class ESPHomeDashboard:
self.ping_request: asyncio.Event | None = None
self.mqtt_ping_request = threading.Event()
self.mdns_status: MDNSStatus | None = None
self.settings: DashboardSettings = DashboardSettings()
self.settings = DashboardSettings()
self.dns_cache = DNSCache()
async def async_setup(self) -> None:
"""Setup the dashboard."""

View File

@ -1,11 +1,19 @@
from __future__ import annotations
import asyncio
import logging
import os
import socket
import threading
import traceback
from asyncio import events
from concurrent.futures import ThreadPoolExecutor
from time import monotonic
from typing import Any
from esphome.storage_json import EsphomeStorageJSON, esphome_storage_path
from .const import MAX_EXECUTOR_WORKERS
from .core import DASHBOARD
from .web_server import make_app, start_web_server
@ -14,6 +22,95 @@ ENV_DEV = "ESPHOME_DASHBOARD_DEV"
settings = DASHBOARD.settings
def can_use_pidfd() -> bool:
"""Check if pidfd_open is available.
Back ported from cpython 3.12
"""
if not hasattr(os, "pidfd_open"):
return False
try:
pid = os.getpid()
os.close(os.pidfd_open(pid, 0))
except OSError:
# blocked by security policy like SECCOMP
return False
return True
class DashboardEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
"""Event loop policy for Home Assistant."""
def __init__(self, debug: bool) -> None:
"""Init the event loop policy."""
super().__init__()
self.debug = debug
self._watcher: asyncio.AbstractChildWatcher | None = None
def _init_watcher(self) -> None:
"""Initialize the watcher for child processes.
Back ported from cpython 3.12
"""
with events._lock: # type: ignore[attr-defined] # pylint: disable=protected-access
if self._watcher is None: # pragma: no branch
if can_use_pidfd():
self._watcher = asyncio.PidfdChildWatcher()
else:
self._watcher = asyncio.ThreadedChildWatcher()
if threading.current_thread() is threading.main_thread():
self._watcher.attach_loop(
self._local._loop # type: ignore[attr-defined] # pylint: disable=protected-access
)
@property
def loop_name(self) -> str:
"""Return name of the loop."""
return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined]
def new_event_loop(self) -> asyncio.AbstractEventLoop:
"""Get the event loop."""
loop: asyncio.AbstractEventLoop = super().new_event_loop()
loop.set_exception_handler(_async_loop_exception_handler)
if self.debug:
loop.set_debug(True)
executor = ThreadPoolExecutor(
thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS
)
loop.set_default_executor(executor)
# bind the built-in time.monotonic directly as loop.time to avoid the
# overhead of the additional method call since its the most called loop
# method and its roughly 10%+ of all the call time in base_events.py
loop.time = monotonic # type: ignore[method-assign]
return loop
def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None:
"""Handle all exception inside the core loop."""
kwargs = {}
if exception := context.get("exception"):
kwargs["exc_info"] = (type(exception), exception, exception.__traceback__)
logger = logging.getLogger(__package__)
if source_traceback := context.get("source_traceback"):
stack_summary = "".join(traceback.format_list(source_traceback))
logger.error(
"Error doing job: %s: %s",
context["message"],
stack_summary,
**kwargs, # type: ignore[arg-type]
)
return
logger.error(
"Error doing job: %s",
context["message"],
**kwargs, # type: ignore[arg-type]
)
def start_dashboard(args) -> None:
"""Start the dashboard."""
settings.parse_args(args)
@ -26,6 +123,8 @@ def start_dashboard(args) -> None:
storage.save(path)
settings.cookie_secret = storage.cookie_secret
asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose))
try:
asyncio.run(async_start(args))
except KeyboardInterrupt:

43
esphome/dashboard/dns.py Normal file
View File

@ -0,0 +1,43 @@
from __future__ import annotations
import asyncio
import sys
from icmplib import NameLookupError, async_resolve
if sys.version_info >= (3, 11):
from asyncio import timeout as async_timeout
else:
from async_timeout import timeout as async_timeout
async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception:
"""Wrap the icmplib async_resolve function."""
try:
async with async_timeout(2):
return await async_resolve(hostname)
except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex:
return ex
class DNSCache:
"""DNS cache for the dashboard."""
def __init__(self, ttl: int | None = 120) -> None:
"""Initialize the DNSCache."""
self._cache: dict[str, tuple[float, list[str] | Exception]] = {}
self._ttl = ttl
async def async_resolve(
self, hostname: str, now_monotonic: float
) -> list[str] | Exception:
"""Resolve a hostname to a list of IP address."""
if expire_time_addresses := self._cache.get(hostname):
expire_time, addresses = expire_time_addresses
if expire_time > now_monotonic:
return addresses
expires = now_monotonic + self._ttl
addresses = await _async_resolve_wrapper(hostname)
self._cache[hostname] = (expires, addresses)
return addresses

View File

@ -14,7 +14,19 @@ from .util.password import password_hash
class DashboardSettings:
"""Settings for the dashboard."""
__slots__ = (
"config_dir",
"password_hash",
"username",
"using_password",
"on_ha_addon",
"cookie_secret",
"absolute_config_dir",
"verbose",
)
def __init__(self) -> None:
"""Initialize the dashboard settings."""
self.config_dir: str = ""
self.password_hash: str = ""
self.username: str = ""
@ -22,8 +34,10 @@ class DashboardSettings:
self.on_ha_addon: bool = False
self.cookie_secret: str | None = None
self.absolute_config_dir: Path | None = None
self.verbose: bool = False
def parse_args(self, args: Any) -> None:
"""Parse the arguments."""
self.on_ha_addon: bool = args.ha_addon
password = args.password or os.getenv("PASSWORD") or ""
if not self.on_ha_addon:
@ -33,6 +47,7 @@ class DashboardSettings:
self.password_hash = password_hash(password)
self.config_dir = args.configuration
self.absolute_config_dir = Path(self.config_dir).resolve()
self.verbose = args.verbose
CORE.config_path = os.path.join(self.config_dir, ".")
@property

View File

@ -1,20 +1,20 @@
from __future__ import annotations
import asyncio
import os
import logging
import time
from typing import cast
from icmplib import Host, SocketPermissionError, async_ping
from ..const import MAX_EXECUTOR_WORKERS
from ..core import DASHBOARD
from ..entries import DashboardEntry, bool_to_entry_state
from ..entries import DashboardEntry, EntryState, bool_to_entry_state
from ..util.itertools import chunked
from ..util.subprocess import async_system_command_status
_LOGGER = logging.getLogger(__name__)
async def _async_ping_host(host: str) -> bool:
"""Ping a host."""
return await async_system_command_status(
["ping", "-n" if os.name == "nt" else "-c", "1", host]
)
GROUP_SIZE = int(MAX_EXECUTOR_WORKERS / 2)
class PingStatus:
@ -27,6 +27,10 @@ class PingStatus:
"""Run the ping status."""
dashboard = DASHBOARD
entries = dashboard.entries
privileged = await _can_use_icmp_lib_with_privilege()
if privileged is None:
_LOGGER.warning("Cannot use icmplib because privileges are insufficient")
return
while not dashboard.stop_event.is_set():
# Only ping if the dashboard is open
@ -36,15 +40,68 @@ class PingStatus:
to_ping: list[DashboardEntry] = [
entry for entry in current_entries if entry.address is not None
]
for ping_group in chunked(to_ping, 16):
# Resolve DNS for all entries
entries_with_addresses: dict[DashboardEntry, list[str]] = {}
for ping_group in chunked(to_ping, GROUP_SIZE):
ping_group = cast(list[DashboardEntry], ping_group)
results = await asyncio.gather(
*(_async_ping_host(entry.address) for entry in ping_group),
now_monotonic = time.monotonic()
dns_results = await asyncio.gather(
*(
dashboard.dns_cache.async_resolve(entry.address, now_monotonic)
for entry in ping_group
),
return_exceptions=True,
)
for entry, result in zip(ping_group, results):
for entry, result in zip(ping_group, dns_results):
if isinstance(result, Exception):
result = False
entries.async_set_state(entry, EntryState.UNKNOWN)
continue
if isinstance(result, BaseException):
raise result
entries_with_addresses[entry] = result
# Ping all entries with valid addresses
for ping_group in chunked(entries_with_addresses.items(), GROUP_SIZE):
entry_addresses = cast(tuple[DashboardEntry, list[str]], ping_group)
results = await asyncio.gather(
*(
async_ping(addresses[0], privileged=privileged)
for _, addresses in entry_addresses
),
return_exceptions=True,
)
for entry_addresses, result in zip(entry_addresses, results):
if isinstance(result, Exception):
ping_result = False
elif isinstance(result, BaseException):
raise result
entries.async_set_state(entry, bool_to_entry_state(result))
else:
host: Host = result
ping_result = host.is_alive
entry, _ = entry_addresses
entries.async_set_state(entry, bool_to_entry_state(ping_result))
async def _can_use_icmp_lib_with_privilege() -> None | bool:
"""Verify we can create a raw socket."""
try:
await async_ping("127.0.0.1", count=0, timeout=0, privileged=True)
except SocketPermissionError:
try:
await async_ping("127.0.0.1", count=0, timeout=0, privileged=False)
except SocketPermissionError:
_LOGGER.debug(
"Cannot use icmplib because privileges are insufficient to create the"
" socket"
)
return None
_LOGGER.debug("Using icmplib in privileged=False mode")
return False
_LOGGER.debug("Using icmplib in privileged=True mode")
return True

View File

@ -9,6 +9,7 @@ import hashlib
import json
import logging
import os
import time
import secrets
import shutil
import subprocess
@ -302,16 +303,28 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
port = json_message["port"]
if (
port == "OTA" # pylint: disable=too-many-boolean-expressions
and (mdns := dashboard.mdns_status)
and (entry := entries.get(config_file))
and entry.loaded_integrations
and "api" in entry.loaded_integrations
and (address := await mdns.async_resolve_host(entry.name))
):
# Use the IP address if available but only
# if the API is loaded and the device is online
# since MQTT logging will not work otherwise
port = address
if (mdns := dashboard.mdns_status) and (
address := await mdns.async_resolve_host(entry.name)
):
# Use the IP address if available but only
# if the API is loaded and the device is online
# since MQTT logging will not work otherwise
port = address
elif (
entry.address
and (
address_list := await dashboard.dns_cache.async_resolve(
entry.address, time.monotonic()
)
)
and not isinstance(address_list, Exception)
):
# If mdns is not available, try to use the DNS cache
port = address_list[0]
return [
*DASHBOARD_COMMAND,

View File

@ -136,7 +136,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
extends = common:idf
platform = platformio/espressif32@5.4.0
platform_packages =
platformio/framework-espidf@~3.40405.0
platformio/framework-espidf@~3.40406.0
framework = espidf
lib_deps =

View File

@ -1,7 +1,9 @@
async_timeout==4.0.3; python_version <= "3.10"
voluptuous==0.14.1
PyYAML==6.0.1
paho-mqtt==1.6.1
colorama==0.4.6
icmplib==3.0.4
tornado==6.4
tzlocal==5.2 # from time
tzdata>=2021.1 # from time

View File

@ -1,13 +1,13 @@
pylint==3.0.3
flake8==6.1.0 # also change in .pre-commit-config.yaml when updating
black==23.12.0 # also change in .pre-commit-config.yaml when updating
flake8==7.0.0 # also change in .pre-commit-config.yaml when updating
black==23.12.1 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==7.4.3
pytest==7.4.4
pytest-cov==4.1.0
pytest-mock==3.12.0
pytest-asyncio==0.23.2
pytest-asyncio==0.23.3
asyncmock==0.4.2
hypothesis==5.49.0
hypothesis==6.92.1

View File

@ -690,7 +690,7 @@ sensor:
update_interval: 30s
mode: low_power
i2c_id: i2c_bus
- platform: bme280
- platform: bme280_i2c
temperature:
name: Outside Temperature
oversampling: 16x
@ -704,6 +704,21 @@ sensor:
iir_filter: 16x
update_interval: 15s
i2c_id: i2c_bus
- platform: bme280_spi
temperature:
name: Outside Temperature
oversampling: 16x
pressure:
name: Outside Pressure
oversampling: none
humidity:
name: Outside Humidity
oversampling: 8x
cs_pin:
allow_other_uses: true
number: GPIO23
iir_filter: 16x
update_interval: 15s
- platform: bme680
temperature:
name: Outside Temperature

View File

@ -1,7 +1,7 @@
import pytest
from hypothesis import given
from hypothesis.provisional import ip_addresses
from hypothesis.strategies import ip_addresses
from strategies import mac_addr_strings
from esphome import core, const

View File

@ -1,7 +1,7 @@
import pytest
from hypothesis import given
from hypothesis.provisional import ip_addresses
from hypothesis.strategies import ip_addresses
from esphome import helpers