mirror of
https://github.com/esphome/esphome.git
synced 2024-11-28 12:46:22 +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
|
||||
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 .
|
||||
|
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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/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
|
@ -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
|
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
|
||||
# - 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),
|
||||
}
|
||||
|
||||
|
@ -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,6 +266,9 @@ 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
|
||||
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)
|
||||
|
||||
|
@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
|
||||
|
||||
DEFAULT_GLYPHS = (
|
||||
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
|
||||
)
|
||||
CONF_RAW_GLYPH_ID = "raw_glyph_id"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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_;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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])
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include "pylontech.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pylontech {
|
||||
@ -34,13 +35,15 @@ void PylontechComponent::setup() {
|
||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||
|
||||
void PylontechComponent::loop() {
|
||||
uint8_t data;
|
||||
|
||||
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
|
||||
@ -48,7 +51,8 @@ void PylontechComponent::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_]);
|
||||
@ -56,6 +60,7 @@ void PylontechComponent::loop() {
|
||||
buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PylontechComponent::process_line_(std::string &buffer) {
|
||||
ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str());
|
||||
@ -66,10 +71,11 @@ void PylontechComponent::process_line_(std::string &buffer) {
|
||||
// clang-format on
|
||||
|
||||
PylontechListener::LineContents l{};
|
||||
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 %d %*s", // 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, &l.mostempr); // 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);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
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 {
|
||||
first = data.data.substr(0, 229);
|
||||
rest = data.data.substr(230);
|
||||
break;
|
||||
}
|
||||
ESP_LOGI(TAG, "Received Pronto: data=%s", first.c_str());
|
||||
if (!rest.empty()) {
|
||||
ESP_LOGI(TAG, "%s", rest.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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."""
|
||||
|
@ -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
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:
|
||||
"""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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
):
|
||||
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,
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user