mirror of
https://github.com/esphome/esphome.git
synced 2024-11-30 13:04:13 +01:00
Merge branch 'dev' into add-graphical-layout-system
This commit is contained in:
commit
97de871a8a
13
.github/actions/restore-python/action.yml
vendored
13
.github/actions/restore-python/action.yml
vendored
@ -28,11 +28,20 @@ runs:
|
|||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
|
||||||
- name: Create Python virtual environment
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
python -m venv venv
|
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
|
python --version
|
||||||
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@ -166,7 +166,35 @@ jobs:
|
|||||||
|
|
||||||
pytest:
|
pytest:
|
||||||
name: Run 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:
|
needs:
|
||||||
- common
|
- common
|
||||||
steps:
|
steps:
|
||||||
@ -175,14 +203,24 @@ jobs:
|
|||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ matrix.python-version }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Register matcher
|
- name: Register matcher
|
||||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||||
- name: Run pytest
|
- 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: |
|
run: |
|
||||||
. venv/bin/activate
|
. 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:
|
clang-format:
|
||||||
name: Check clang-format
|
name: Check clang-format
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.12.0
|
rev: 23.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args:
|
args:
|
||||||
|
@ -54,6 +54,8 @@ esphome/components/bl0940/* @tobias-
|
|||||||
esphome/components/bl0942/* @dbuezas
|
esphome/components/bl0942/* @dbuezas
|
||||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||||
esphome/components/bluetooth_proxy/* @jesserockz
|
esphome/components/bluetooth_proxy/* @jesserockz
|
||||||
|
esphome/components/bme280_base/* @esphome/core
|
||||||
|
esphome/components/bme280_spi/* @apbodrov
|
||||||
esphome/components/bme680_bsec/* @trvrnrth
|
esphome/components/bme680_bsec/* @trvrnrth
|
||||||
esphome/components/bmi160/* @flaviut
|
esphome/components/bmi160/* @flaviut
|
||||||
esphome/components/bmp3xx/* @martgras
|
esphome/components/bmp3xx/* @martgras
|
||||||
|
@ -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]))
|
|
1
esphome/components/bme280_base/__init__.py
Normal file
1
esphome/components/bme280_base/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@esphome/core"]
|
@ -1,9 +1,14 @@
|
|||||||
#include "bme280.h"
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "bme280_base.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include <esphome/components/sensor/sensor.h>
|
||||||
|
#include <esphome/core/component.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace bme280 {
|
namespace bme280_base {
|
||||||
|
|
||||||
static const char *const TAG = "bme280.sensor";
|
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); }
|
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) {
|
switch (oversampling) {
|
||||||
case BME280_OVERSAMPLING_NONE:
|
case BME280_OVERSAMPLING_NONE:
|
||||||
return "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() {
|
void BME280Component::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Setting up BME280...");
|
ESP_LOGCONFIG(TAG, "Setting up BME280...");
|
||||||
uint8_t chip_id = 0;
|
uint8_t chip_id = 0;
|
||||||
@ -112,7 +117,7 @@ void BME280Component::setup() {
|
|||||||
// Wait until the NVM data has finished loading.
|
// Wait until the NVM data has finished loading.
|
||||||
uint8_t status;
|
uint8_t status;
|
||||||
uint8_t retry = 5;
|
uint8_t retry = 5;
|
||||||
do {
|
do { // NOLINT
|
||||||
delay(2);
|
delay(2);
|
||||||
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
||||||
ESP_LOGW(TAG, "Error reading status register.");
|
ESP_LOGW(TAG, "Error reading status register.");
|
||||||
@ -175,7 +180,6 @@ void BME280Component::setup() {
|
|||||||
}
|
}
|
||||||
void BME280Component::dump_config() {
|
void BME280Component::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "BME280:");
|
ESP_LOGCONFIG(TAG, "BME280:");
|
||||||
LOG_I2C_DEVICE(this);
|
|
||||||
switch (this->error_code_) {
|
switch (this->error_code_) {
|
||||||
case COMMUNICATION_FAILED:
|
case COMMUNICATION_FAILED:
|
||||||
ESP_LOGE(TAG, "Communication with BME280 failed!");
|
ESP_LOGE(TAG, "Communication with BME280 failed!");
|
||||||
@ -226,14 +230,14 @@ void BME280Component::update() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int32_t t_fine = 0;
|
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)) {
|
if (std::isnan(temperature)) {
|
||||||
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
|
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
|
||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float pressure = this->read_pressure_(data, t_fine);
|
float const pressure = this->read_pressure_(data, t_fine);
|
||||||
float humidity = this->read_humidity_(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);
|
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
|
||||||
if (this->temperature_sensor_ != nullptr)
|
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 t2 = this->calibration_.t2;
|
||||||
const int32_t t3 = this->calibration_.t3;
|
const int32_t t3 = this->calibration_.t3;
|
||||||
|
|
||||||
int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
|
int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
|
||||||
int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
|
int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
|
||||||
*t_fine = var1 + var2;
|
*t_fine = var1 + var2;
|
||||||
|
|
||||||
float temperature = (*t_fine * 5 + 128) >> 8;
|
float const temperature = (*t_fine * 5 + 128) >> 8;
|
||||||
return temperature / 100.0f;
|
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) {
|
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)
|
if (raw_adc == 0x8000)
|
||||||
return NAN;
|
return NAN;
|
||||||
|
|
||||||
int32_t adc = raw_adc;
|
int32_t const adc = raw_adc;
|
||||||
|
|
||||||
const int32_t h1 = this->calibration_.h1;
|
const int32_t h1 = this->calibration_.h1;
|
||||||
const int32_t h2 = this->calibration_.h2;
|
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 < 0 ? 0 : v_x1_u32r;
|
||||||
v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : 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;
|
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); }
|
int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
|
||||||
|
|
||||||
} // namespace bme280
|
} // namespace bme280_base
|
||||||
} // namespace esphome
|
} // namespace esphome
|
@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
#include "esphome/components/i2c/i2c.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace bme280 {
|
namespace bme280_base {
|
||||||
|
|
||||||
/// Internal struct storing the calibration values of an BME280.
|
/// Internal struct storing the calibration values of an BME280.
|
||||||
struct BME280CalibrationData {
|
struct BME280CalibrationData {
|
||||||
@ -57,8 +56,8 @@ enum BME280IIRFilter {
|
|||||||
BME280_IIR_FILTER_16X = 0b100,
|
BME280_IIR_FILTER_16X = 0b100,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor.
|
/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor.
|
||||||
class BME280Component : public PollingComponent, public i2c::I2CDevice {
|
class BME280Component : public PollingComponent {
|
||||||
public:
|
public:
|
||||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_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);
|
uint16_t read_u16_le_(uint8_t a_register);
|
||||||
int16_t read_s16_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_;
|
BME280CalibrationData calibration_;
|
||||||
BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X};
|
BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X};
|
||||||
BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
|
BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
|
||||||
@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
|
|||||||
} error_code_{NONE};
|
} error_code_{NONE};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace bme280
|
} // namespace bme280_base
|
||||||
} // namespace esphome
|
} // namespace esphome
|
106
esphome/components/bme280_base/sensor.py
Normal file
106
esphome/components/bme280_base/sensor.py
Normal 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]))
|
30
esphome/components/bme280_i2c/bme280_i2c.cpp
Normal file
30
esphome/components/bme280_i2c/bme280_i2c.cpp
Normal 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
|
20
esphome/components/bme280_i2c/bme280_i2c.h
Normal file
20
esphome/components/bme280_i2c/bme280_i2c.h
Normal 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
|
19
esphome/components/bme280_i2c/sensor.py
Normal file
19
esphome/components/bme280_i2c/sensor.py
Normal 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)
|
1
esphome/components/bme280_spi/__init__.py
Normal file
1
esphome/components/bme280_spi/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@apbodrov"]
|
66
esphome/components/bme280_spi/bme280_spi.cpp
Normal file
66
esphome/components/bme280_spi/bme280_spi.cpp
Normal 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
|
20
esphome/components/bme280_spi/bme280_spi.h
Normal file
20
esphome/components/bme280_spi/bme280_spi.h
Normal 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
|
24
esphome/components/bme280_spi/sensor.py
Normal file
24
esphome/components/bme280_spi/sensor.py
Normal 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)
|
@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
|
|||||||
# The default/recommended esp-idf framework version
|
# The default/recommended esp-idf framework version
|
||||||
# - https://github.com/espressif/esp-idf/releases
|
# - https://github.com/espressif/esp-idf/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
|
# - 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
|
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||||
# - https://github.com/platformio/platform-espressif32/releases
|
# - https://github.com/platformio/platform-espressif32/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
# - 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):
|
def _esp_idf_check_versions(value):
|
||||||
value = value.copy()
|
value = value.copy()
|
||||||
lookups = {
|
lookups = {
|
||||||
"dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"),
|
"dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"),
|
||||||
"latest": (cv.Version(5, 1, 0), None),
|
"latest": (cv.Version(5, 1, 2), None),
|
||||||
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
|
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
|
from esphome.components import esp32_ble
|
||||||
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ACTIVE,
|
CONF_ACTIVE,
|
||||||
|
CONF_DURATION,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_INTERVAL,
|
CONF_INTERVAL,
|
||||||
CONF_DURATION,
|
|
||||||
CONF_TRIGGER_ID,
|
|
||||||
CONF_MAC_ADDRESS,
|
CONF_MAC_ADDRESS,
|
||||||
CONF_SERVICE_UUID,
|
|
||||||
CONF_MANUFACTURER_ID,
|
CONF_MANUFACTURER_ID,
|
||||||
CONF_ON_BLE_ADVERTISE,
|
CONF_ON_BLE_ADVERTISE,
|
||||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
|
||||||
CONF_ON_BLE_MANUFACTURER_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.core import CORE
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
|
||||||
|
|
||||||
AUTO_LOAD = ["esp32_ble"]
|
AUTO_LOAD = ["esp32_ble"]
|
||||||
DEPENDENCIES = ["esp32"]
|
DEPENDENCIES = ["esp32"]
|
||||||
@ -263,7 +266,10 @@ async def to_code(config):
|
|||||||
# https://github.com/espressif/esp-idf/issues/2503
|
# https://github.com/espressif/esp-idf/issues/2503
|
||||||
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
||||||
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
|
# 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)
|
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
|
||||||
|
|
||||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||||
|
@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema)
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_GLYPHS = (
|
DEFAULT_GLYPHS = (
|
||||||
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||||
)
|
)
|
||||||
CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@ DEPENDENCIES = ["i2s_audio"]
|
|||||||
CONF_ADC_PIN = "adc_pin"
|
CONF_ADC_PIN = "adc_pin"
|
||||||
CONF_ADC_TYPE = "adc_type"
|
CONF_ADC_TYPE = "adc_type"
|
||||||
CONF_PDM = "pdm"
|
CONF_PDM = "pdm"
|
||||||
|
CONF_SAMPLE_RATE = "sample_rate"
|
||||||
CONF_BITS_PER_SAMPLE = "bits_per_sample"
|
CONF_BITS_PER_SAMPLE = "bits_per_sample"
|
||||||
|
CONF_USE_APLL = "use_apll"
|
||||||
|
|
||||||
I2SAudioMicrophone = i2s_audio_ns.class_(
|
I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||||
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
||||||
@ -62,9 +64,11 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
|||||||
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
|
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
|
||||||
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
|
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
|
||||||
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
|
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(
|
cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All(
|
||||||
_validate_bits, cv.enum(BITS_PER_SAMPLE)
|
_validate_bits, cv.enum(BITS_PER_SAMPLE)
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).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_pdm(config[CONF_PDM]))
|
||||||
|
|
||||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
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_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
|
||||||
|
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
|
||||||
|
|
||||||
await microphone.register_microphone(var, config)
|
await microphone.register_microphone(var, config)
|
||||||
|
@ -47,14 +47,14 @@ void I2SAudioMicrophone::start_() {
|
|||||||
}
|
}
|
||||||
i2s_driver_config_t config = {
|
i2s_driver_config_t config = {
|
||||||
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
|
.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_,
|
.bits_per_sample = this->bits_per_sample_,
|
||||||
.channel_format = this->channel_,
|
.channel_format = this->channel_,
|
||||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||||
.dma_buf_count = 4,
|
.dma_buf_count = 4,
|
||||||
.dma_buf_len = 256,
|
.dma_buf_len = 256,
|
||||||
.use_apll = false,
|
.use_apll = this->use_apll_,
|
||||||
.tx_desc_auto_clear = false,
|
.tx_desc_auto_clear = false,
|
||||||
.fixed_mclk = 0,
|
.fixed_mclk = 0,
|
||||||
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
|
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
|
||||||
|
@ -31,7 +31,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
|
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_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:
|
protected:
|
||||||
void start_();
|
void start_();
|
||||||
@ -45,7 +47,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
#endif
|
#endif
|
||||||
bool pdm_{false};
|
bool pdm_{false};
|
||||||
i2s_channel_fmt_t channel_;
|
i2s_channel_fmt_t channel_;
|
||||||
|
uint32_t sample_rate_;
|
||||||
i2s_bits_per_sample_t bits_per_sample_;
|
i2s_bits_per_sample_t bits_per_sample_;
|
||||||
|
bool use_apll_;
|
||||||
|
|
||||||
HighFrequencyLoopRequester high_freq_;
|
HighFrequencyLoopRequester high_freq_;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
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_width() const override;
|
||||||
int get_height() const override;
|
int get_height() const override;
|
||||||
|
const uint8_t *get_data_start() { return this->data_start_; }
|
||||||
ImageType get_type() const;
|
ImageType get_type() const;
|
||||||
|
|
||||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||||
|
@ -309,7 +309,7 @@ async def component_to_code(config):
|
|||||||
lt_options["LT_UART_SILENT_ENABLED"] = 0
|
lt_options["LT_UART_SILENT_ENABLED"] = 0
|
||||||
lt_options["LT_UART_SILENT_ALL"] = 0
|
lt_options["LT_UART_SILENT_ALL"] = 0
|
||||||
# set default UART port
|
# 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
|
lt_options["LT_UART_DEFAULT_PORT"] = uart_port
|
||||||
# add custom options
|
# add custom options
|
||||||
lt_options.update(framework[CONF_OPTIONS])
|
lt_options.update(framework[CONF_OPTIONS])
|
||||||
|
@ -2,6 +2,7 @@ import esphome.codegen as cg
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.components import display, uart
|
from esphome.components import display, uart
|
||||||
|
from esphome.components import esp32
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
@ -96,6 +97,11 @@ async def to_code(config):
|
|||||||
if CORE.is_esp32 and CORE.using_arduino:
|
if CORE.is_esp32 and CORE.using_arduino:
|
||||||
cg.add_library("WiFiClientSecure", None)
|
cg.add_library("WiFiClientSecure", None)
|
||||||
cg.add_library("HTTPClient", 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:
|
elif CORE.is_esp8266 and CORE.using_arduino:
|
||||||
cg.add_library("ESP8266HTTPClient", None)
|
cg.add_library("ESP8266HTTPClient", None)
|
||||||
|
|
||||||
|
@ -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);
|
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.
|
/** Set the brightness of the backlight.
|
||||||
*
|
*
|
||||||
* @param brightness The brightness percentage from 0 to 1.0.
|
* @param brightness The brightness percentage from 0 to 1.0.
|
||||||
|
@ -294,6 +294,19 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color)
|
|||||||
display::ColorUtil::color_to_565(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) {
|
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_("rtc0", "rtc0=%u", time.year);
|
||||||
this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month);
|
this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month);
|
||||||
|
@ -24,7 +24,7 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
|||||||
ESP_LOGVV(TAG, "url: %s", url.c_str());
|
ESP_LOGVV(TAG, "url: %s", url.c_str());
|
||||||
uint range_size = this->tft_size_ - range_start;
|
uint range_size = this->tft_size_ - range_start;
|
||||||
ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_);
|
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_;
|
int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_;
|
||||||
if (range_size <= 0 or range_end <= range_start) {
|
if (range_size <= 0 or range_end <= range_start) {
|
||||||
ESP_LOGE(TAG, "Invalid range");
|
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 = {
|
esp_http_client_config_t config = {
|
||||||
.url = url.c_str(),
|
.url = url.c_str(),
|
||||||
.cert_pem = nullptr,
|
.cert_pem = nullptr,
|
||||||
|
.disable_auto_redirect = false,
|
||||||
|
.max_redirection_count = 10,
|
||||||
};
|
};
|
||||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
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);
|
sprintf(range_header, "bytes=%d-%d", range_start, range_end);
|
||||||
ESP_LOGV(TAG, "Requesting range: %s", range_header);
|
ESP_LOGV(TAG, "Requesting range: %s", range_header);
|
||||||
esp_http_client_set_header(client, "Range", 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_LOGV(TAG, "Opening http connetion");
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
@ -70,13 +72,13 @@ int Nextion::upload_range(const std::string &url, int range_start) {
|
|||||||
std::string recv_string;
|
std::string recv_string;
|
||||||
if (buffer == nullptr) {
|
if (buffer == nullptr) {
|
||||||
ESP_LOGE(TAG, "Failed to allocate memory for buffer");
|
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 {
|
} else {
|
||||||
ESP_LOGV(TAG, "Memory for buffer allocated successfully");
|
ESP_LOGV(TAG, "Memory for buffer allocated successfully");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
App.feed_wdt();
|
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);
|
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);
|
ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len);
|
||||||
if (read_len > 0) {
|
if (read_len > 0) {
|
||||||
@ -145,17 +147,19 @@ bool Nextion::upload_tft() {
|
|||||||
|
|
||||||
// Define the configuration for the HTTP client
|
// Define the configuration for the HTTP client
|
||||||
ESP_LOGV(TAG, "Establishing connection to HTTP server");
|
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 = {
|
esp_http_client_config_t config = {
|
||||||
.url = this->tft_url_.c_str(),
|
.url = this->tft_url_.c_str(),
|
||||||
.cert_pem = nullptr,
|
.cert_pem = nullptr,
|
||||||
.method = HTTP_METHOD_HEAD,
|
.method = HTTP_METHOD_HEAD,
|
||||||
.timeout_ms = 15000,
|
.timeout_ms = 15000,
|
||||||
|
.disable_auto_redirect = false,
|
||||||
|
.max_redirection_count = 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the HTTP client with the configuration
|
// Initialize the HTTP client with the configuration
|
||||||
ESP_LOGV(TAG, "Initializing HTTP client");
|
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);
|
esp_http_client_handle_t http = esp_http_client_init(&config);
|
||||||
if (!http) {
|
if (!http) {
|
||||||
ESP_LOGE(TAG, "Failed to initialize HTTP client.");
|
ESP_LOGE(TAG, "Failed to initialize HTTP client.");
|
||||||
@ -164,7 +168,7 @@ bool Nextion::upload_tft() {
|
|||||||
|
|
||||||
// Perform the HTTP request
|
// Perform the HTTP request
|
||||||
ESP_LOGV(TAG, "Check if the client could connect");
|
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);
|
esp_err_t err = esp_http_client_perform(http);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
|
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();
|
this->soft_reset();
|
||||||
vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT
|
vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT
|
||||||
if (successful) {
|
if (successful) {
|
||||||
ESP_LOGD(TAG, "Restarting esphome");
|
ESP_LOGD(TAG, "Restarting ESPHome");
|
||||||
esp_restart(); // NOLINT(readability-static-accessed-through-instance)
|
esp_restart(); // NOLINT(readability-static-accessed-through-instance)
|
||||||
}
|
}
|
||||||
return successful;
|
return successful;
|
||||||
|
@ -19,7 +19,7 @@ PylontechComponent = pylontech_ns.class_(
|
|||||||
)
|
)
|
||||||
PylontechBattery = pylontech_ns.class_("PylontechBattery")
|
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(
|
PYLONTECH_COMPONENT_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include "pylontech.h"
|
#include "pylontech.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace pylontech {
|
namespace pylontech {
|
||||||
@ -34,26 +35,30 @@ void PylontechComponent::setup() {
|
|||||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||||
|
|
||||||
void PylontechComponent::loop() {
|
void PylontechComponent::loop() {
|
||||||
uint8_t data;
|
if (this->available() > 0) {
|
||||||
|
// pylontech sends a lot of data very suddenly
|
||||||
// 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
|
||||||
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
uint8_t data;
|
||||||
while (this->available() > 0) {
|
int recv = 0;
|
||||||
if (this->read_byte(&data)) {
|
while (this->available() > 0) {
|
||||||
buffer_[buffer_index_write_] += (char) data;
|
if (this->read_byte(&data)) {
|
||||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
buffer_[buffer_index_write_] += (char) data;
|
||||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
recv++;
|
||||||
// complete line received
|
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
||||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||||
|
// complete line received
|
||||||
|
buffer_index_write_ = (buffer_index_write_ + 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
|
// only process one line per call of loop() to not block esphome for too long
|
||||||
if (buffer_index_read_ != buffer_index_write_) {
|
if (buffer_index_read_ != buffer_index_write_) {
|
||||||
this->process_line_(buffer_[buffer_index_read_]);
|
this->process_line_(buffer_[buffer_index_read_]);
|
||||||
buffer_[buffer_index_read_].clear();
|
buffer_[buffer_index_read_].clear();
|
||||||
buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
|
buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,10 +71,11 @@ void PylontechComponent::process_line_(std::string &buffer) {
|
|||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
PylontechListener::LineContents l{};
|
PylontechListener::LineContents l{};
|
||||||
const int parsed = sscanf( // NOLINT
|
char mostempr_s[6];
|
||||||
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
|
const int parsed = sscanf( // 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
|
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.curr_st, l.temp_st, &l.coulomb, &l.mostempr); // 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) {
|
if (l.bat_num <= 0) {
|
||||||
ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str());
|
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());
|
ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str());
|
||||||
return;
|
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_) {
|
for (PylontechListener *listener : this->listeners_) {
|
||||||
listener->on_line_read(&l);
|
listener->on_line_read(&l);
|
||||||
|
@ -59,14 +59,14 @@ TYPES: dict[str, cv.Schema] = {
|
|||||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
),
|
),
|
||||||
CONF_VOLTAGE_LOW: sensor.sensor_schema(
|
CONF_VOLTAGE_LOW: sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
unit_of_measurement=UNIT_VOLT,
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=3,
|
||||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
device_class=DEVICE_CLASS_VOLTAGE,
|
||||||
),
|
),
|
||||||
CONF_VOLTAGE_HIGH: sensor.sensor_schema(
|
CONF_VOLTAGE_HIGH: sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
unit_of_measurement=UNIT_VOLT,
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=3,
|
||||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
device_class=DEVICE_CLASS_VOLTAGE,
|
||||||
),
|
),
|
||||||
CONF_COULOMB: sensor.sensor_schema(
|
CONF_COULOMB: sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PERCENT,
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
|
@ -227,16 +227,17 @@ optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProntoProtocol::dump(const ProntoData &data) {
|
void ProntoProtocol::dump(const ProntoData &data) {
|
||||||
std::string first, rest;
|
std::string rest;
|
||||||
if (data.data.size() < 230) {
|
|
||||||
first = data.data;
|
rest = data.data;
|
||||||
} else {
|
ESP_LOGI(TAG, "Received Pronto: data=");
|
||||||
first = data.data.substr(0, 229);
|
while (true) {
|
||||||
rest = data.data.substr(230);
|
ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str());
|
||||||
}
|
if (rest.size() > 230) {
|
||||||
ESP_LOGI(TAG, "Received Pronto: data=%s", first.c_str());
|
rest = rest.substr(230);
|
||||||
if (!rest.empty()) {
|
} else {
|
||||||
ESP_LOGI(TAG, "%s", rest.c_str());
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ class UARTComponent {
|
|||||||
// @return Baud rate in bits per second.
|
// @return Baud rate in bits per second.
|
||||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||||
/**
|
/**
|
||||||
* Load the UART settings.
|
* Load the UART settings.
|
||||||
* @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly.
|
* @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).
|
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
|
||||||
*/
|
*/
|
||||||
virtual void load_settings(){};
|
virtual void load_settings(){};
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP8266 || USE_ESP32
|
||||||
|
|
||||||
#ifdef USE_UART_DEBUGGER
|
#ifdef USE_UART_DEBUGGER
|
||||||
void add_debug_callback(std::function<void(UARTDirection, uint8_t)> &&callback) {
|
void add_debug_callback(std::function<void(UARTDirection, uint8_t)> &&callback) {
|
||||||
|
@ -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() {
|
void ESP8266UartComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "UART Bus:");
|
ESP_LOGCONFIG(TAG, "UART Bus:");
|
||||||
LOG_PIN(" TX Pin: ", tx_pin_);
|
LOG_PIN(" TX Pin: ", this->tx_pin_);
|
||||||
LOG_PIN(" RX Pin: ", rx_pin_);
|
LOG_PIN(" RX Pin: ", this->rx_pin_);
|
||||||
if (this->rx_pin_ != nullptr) {
|
if (this->rx_pin_ != nullptr) {
|
||||||
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT
|
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,21 @@ class ESP8266UartComponent : public UARTComponent, public Component {
|
|||||||
|
|
||||||
uint32_t get_config();
|
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:
|
protected:
|
||||||
void check_logger_conflict() override;
|
void check_logger_conflict() override;
|
||||||
|
|
||||||
|
@ -4,5 +4,7 @@ EVENT_ENTRY_ADDED = "entry_added"
|
|||||||
EVENT_ENTRY_REMOVED = "entry_removed"
|
EVENT_ENTRY_REMOVED = "entry_removed"
|
||||||
EVENT_ENTRY_UPDATED = "entry_updated"
|
EVENT_ENTRY_UPDATED = "entry_updated"
|
||||||
EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
|
EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
|
||||||
|
MAX_EXECUTOR_WORKERS = 48
|
||||||
|
|
||||||
|
|
||||||
SENTINEL = object()
|
SENTINEL = object()
|
||||||
|
@ -8,6 +8,7 @@ from functools import partial
|
|||||||
from typing import TYPE_CHECKING, Any, Callable
|
from typing import TYPE_CHECKING, Any, Callable
|
||||||
|
|
||||||
from ..zeroconf import DiscoveredImport
|
from ..zeroconf import DiscoveredImport
|
||||||
|
from .dns import DNSCache
|
||||||
from .entries import DashboardEntries
|
from .entries import DashboardEntries
|
||||||
from .settings import DashboardSettings
|
from .settings import DashboardSettings
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ class ESPHomeDashboard:
|
|||||||
"mqtt_ping_request",
|
"mqtt_ping_request",
|
||||||
"mdns_status",
|
"mdns_status",
|
||||||
"settings",
|
"settings",
|
||||||
|
"dns_cache",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -81,7 +83,8 @@ class ESPHomeDashboard:
|
|||||||
self.ping_request: asyncio.Event | None = None
|
self.ping_request: asyncio.Event | None = None
|
||||||
self.mqtt_ping_request = threading.Event()
|
self.mqtt_ping_request = threading.Event()
|
||||||
self.mdns_status: MDNSStatus | None = None
|
self.mdns_status: MDNSStatus | None = None
|
||||||
self.settings: DashboardSettings = DashboardSettings()
|
self.settings = DashboardSettings()
|
||||||
|
self.dns_cache = DNSCache()
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Setup the dashboard."""
|
"""Setup the dashboard."""
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
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 esphome.storage_json import EsphomeStorageJSON, esphome_storage_path
|
||||||
|
|
||||||
|
from .const import MAX_EXECUTOR_WORKERS
|
||||||
from .core import DASHBOARD
|
from .core import DASHBOARD
|
||||||
from .web_server import make_app, start_web_server
|
from .web_server import make_app, start_web_server
|
||||||
|
|
||||||
@ -14,6 +22,95 @@ ENV_DEV = "ESPHOME_DASHBOARD_DEV"
|
|||||||
settings = DASHBOARD.settings
|
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:
|
def start_dashboard(args) -> None:
|
||||||
"""Start the dashboard."""
|
"""Start the dashboard."""
|
||||||
settings.parse_args(args)
|
settings.parse_args(args)
|
||||||
@ -26,6 +123,8 @@ def start_dashboard(args) -> None:
|
|||||||
storage.save(path)
|
storage.save(path)
|
||||||
settings.cookie_secret = storage.cookie_secret
|
settings.cookie_secret = storage.cookie_secret
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.run(async_start(args))
|
asyncio.run(async_start(args))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
43
esphome/dashboard/dns.py
Normal file
43
esphome/dashboard/dns.py
Normal 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
|
@ -14,7 +14,19 @@ from .util.password import password_hash
|
|||||||
class DashboardSettings:
|
class DashboardSettings:
|
||||||
"""Settings for the dashboard."""
|
"""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:
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the dashboard settings."""
|
||||||
self.config_dir: str = ""
|
self.config_dir: str = ""
|
||||||
self.password_hash: str = ""
|
self.password_hash: str = ""
|
||||||
self.username: str = ""
|
self.username: str = ""
|
||||||
@ -22,8 +34,10 @@ class DashboardSettings:
|
|||||||
self.on_ha_addon: bool = False
|
self.on_ha_addon: bool = False
|
||||||
self.cookie_secret: str | None = None
|
self.cookie_secret: str | None = None
|
||||||
self.absolute_config_dir: Path | None = None
|
self.absolute_config_dir: Path | None = None
|
||||||
|
self.verbose: bool = False
|
||||||
|
|
||||||
def parse_args(self, args: Any) -> None:
|
def parse_args(self, args: Any) -> None:
|
||||||
|
"""Parse the arguments."""
|
||||||
self.on_ha_addon: bool = args.ha_addon
|
self.on_ha_addon: bool = args.ha_addon
|
||||||
password = args.password or os.getenv("PASSWORD") or ""
|
password = args.password or os.getenv("PASSWORD") or ""
|
||||||
if not self.on_ha_addon:
|
if not self.on_ha_addon:
|
||||||
@ -33,6 +47,7 @@ class DashboardSettings:
|
|||||||
self.password_hash = password_hash(password)
|
self.password_hash = password_hash(password)
|
||||||
self.config_dir = args.configuration
|
self.config_dir = args.configuration
|
||||||
self.absolute_config_dir = Path(self.config_dir).resolve()
|
self.absolute_config_dir = Path(self.config_dir).resolve()
|
||||||
|
self.verbose = args.verbose
|
||||||
CORE.config_path = os.path.join(self.config_dir, ".")
|
CORE.config_path = os.path.join(self.config_dir, ".")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import logging
|
||||||
|
import time
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
|
from icmplib import Host, SocketPermissionError, async_ping
|
||||||
|
|
||||||
|
from ..const import MAX_EXECUTOR_WORKERS
|
||||||
from ..core import DASHBOARD
|
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.itertools import chunked
|
||||||
from ..util.subprocess import async_system_command_status
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def _async_ping_host(host: str) -> bool:
|
GROUP_SIZE = int(MAX_EXECUTOR_WORKERS / 2)
|
||||||
"""Ping a host."""
|
|
||||||
return await async_system_command_status(
|
|
||||||
["ping", "-n" if os.name == "nt" else "-c", "1", host]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PingStatus:
|
class PingStatus:
|
||||||
@ -27,6 +27,10 @@ class PingStatus:
|
|||||||
"""Run the ping status."""
|
"""Run the ping status."""
|
||||||
dashboard = DASHBOARD
|
dashboard = DASHBOARD
|
||||||
entries = dashboard.entries
|
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():
|
while not dashboard.stop_event.is_set():
|
||||||
# Only ping if the dashboard is open
|
# Only ping if the dashboard is open
|
||||||
@ -36,15 +40,68 @@ class PingStatus:
|
|||||||
to_ping: list[DashboardEntry] = [
|
to_ping: list[DashboardEntry] = [
|
||||||
entry for entry in current_entries if entry.address is not None
|
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)
|
ping_group = cast(list[DashboardEntry], ping_group)
|
||||||
results = await asyncio.gather(
|
now_monotonic = time.monotonic()
|
||||||
*(_async_ping_host(entry.address) for entry in ping_group),
|
dns_results = await asyncio.gather(
|
||||||
|
*(
|
||||||
|
dashboard.dns_cache.async_resolve(entry.address, now_monotonic)
|
||||||
|
for entry in ping_group
|
||||||
|
),
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
for entry, result in zip(ping_group, results):
|
|
||||||
|
for entry, result in zip(ping_group, dns_results):
|
||||||
if isinstance(result, Exception):
|
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):
|
elif isinstance(result, BaseException):
|
||||||
raise result
|
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
|
||||||
|
@ -9,6 +9,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -302,16 +303,28 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
|||||||
port = json_message["port"]
|
port = json_message["port"]
|
||||||
if (
|
if (
|
||||||
port == "OTA" # pylint: disable=too-many-boolean-expressions
|
port == "OTA" # pylint: disable=too-many-boolean-expressions
|
||||||
and (mdns := dashboard.mdns_status)
|
|
||||||
and (entry := entries.get(config_file))
|
and (entry := entries.get(config_file))
|
||||||
and entry.loaded_integrations
|
and entry.loaded_integrations
|
||||||
and "api" in 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 (mdns := dashboard.mdns_status) and (
|
||||||
# if the API is loaded and the device is online
|
address := await mdns.async_resolve_host(entry.name)
|
||||||
# since MQTT logging will not work otherwise
|
):
|
||||||
port = address
|
# 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 [
|
return [
|
||||||
*DASHBOARD_COMMAND,
|
*DASHBOARD_COMMAND,
|
||||||
|
@ -136,7 +136,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
|||||||
extends = common:idf
|
extends = common:idf
|
||||||
platform = platformio/espressif32@5.4.0
|
platform = platformio/espressif32@5.4.0
|
||||||
platform_packages =
|
platform_packages =
|
||||||
platformio/framework-espidf@~3.40405.0
|
platformio/framework-espidf@~3.40406.0
|
||||||
|
|
||||||
framework = espidf
|
framework = espidf
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
async_timeout==4.0.3; python_version <= "3.10"
|
||||||
voluptuous==0.14.1
|
voluptuous==0.14.1
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
paho-mqtt==1.6.1
|
paho-mqtt==1.6.1
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
|
icmplib==3.0.4
|
||||||
tornado==6.4
|
tornado==6.4
|
||||||
tzlocal==5.2 # from time
|
tzlocal==5.2 # from time
|
||||||
tzdata>=2021.1 # from time
|
tzdata>=2021.1 # from time
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
pylint==3.0.3
|
pylint==3.0.3
|
||||||
flake8==6.1.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.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
|
pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
pytest==7.4.3
|
pytest==7.4.4
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
pytest-mock==3.12.0
|
pytest-mock==3.12.0
|
||||||
pytest-asyncio==0.23.2
|
pytest-asyncio==0.23.3
|
||||||
asyncmock==0.4.2
|
asyncmock==0.4.2
|
||||||
hypothesis==5.49.0
|
hypothesis==6.92.1
|
||||||
|
@ -690,7 +690,7 @@ sensor:
|
|||||||
update_interval: 30s
|
update_interval: 30s
|
||||||
mode: low_power
|
mode: low_power
|
||||||
i2c_id: i2c_bus
|
i2c_id: i2c_bus
|
||||||
- platform: bme280
|
- platform: bme280_i2c
|
||||||
temperature:
|
temperature:
|
||||||
name: Outside Temperature
|
name: Outside Temperature
|
||||||
oversampling: 16x
|
oversampling: 16x
|
||||||
@ -704,6 +704,21 @@ sensor:
|
|||||||
iir_filter: 16x
|
iir_filter: 16x
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
i2c_id: i2c_bus
|
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
|
- platform: bme680
|
||||||
temperature:
|
temperature:
|
||||||
name: Outside Temperature
|
name: Outside Temperature
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
from hypothesis.provisional import ip_addresses
|
from hypothesis.strategies import ip_addresses
|
||||||
from strategies import mac_addr_strings
|
from strategies import mac_addr_strings
|
||||||
|
|
||||||
from esphome import core, const
|
from esphome import core, const
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
from hypothesis.provisional import ip_addresses
|
from hypothesis.strategies import ip_addresses
|
||||||
|
|
||||||
from esphome import helpers
|
from esphome import helpers
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user