Merge branch 'dev' into jesserockz-2023-304

This commit is contained in:
Jesse Hills 2023-09-12 09:50:22 +12:00 committed by GitHub
commit 3941f16465
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 2541 additions and 620 deletions

View file

@ -8,7 +8,7 @@ on:
branches: [dev, beta, release]
paths:
- "docker/**"
- ".github/workflows/**"
- ".github/workflows/ci-docker.yml"
- "requirements*.txt"
- "platformio.ini"
- "script/platformio_install_deps.py"
@ -16,7 +16,7 @@ on:
pull_request:
paths:
- "docker/**"
- ".github/workflows/**"
- ".github/workflows/ci-docker.yml"
- "requirements*.txt"
- "platformio.ini"
- "script/platformio_install_deps.py"

View file

@ -41,7 +41,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.1
uses: actions/cache@v3.3.2
with:
path: venv
# yamllint disable-line rule:line-length
@ -232,7 +232,7 @@ jobs:
fail-fast: false
max-parallel: 2
matrix:
file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8, 9, 9.1]
file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8, 9, 9.1, 10]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4
@ -302,7 +302,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache@v3.3.1
uses: actions/cache@v3.3.2
with:
path: ~/.platformio
# yamllint disable-line rule:line-length

6
.gitignore vendored
View file

@ -13,6 +13,12 @@ __pycache__/
# Intellij Idea
.idea
# Eclipse
.project
.cproject
.pydevproject
.settings/
# Vim
*.swp

View file

@ -2,7 +2,7 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.7.0
hooks:
- id: black

View file

@ -49,6 +49,7 @@ esphome/components/bl0942/* @dbuezas
esphome/components/ble_client/* @buxtronix
esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bmi160/* @flaviut
esphome/components/bmp3xx/* @martgras
esphome/components/bmp581/* @kahrendt
esphome/components/bp1658cj/* @Cossid
@ -270,6 +271,8 @@ esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/speaker/* @jesserockz
esphome/components/spi/* @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81
@ -330,6 +333,7 @@ esphome/components/web_server_idf/* @dentra
esphome/components/whirlpool/* @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
esphome/components/wl_134/* @hobbypunk90
esphome/components/x9c/* @EtienneMD
esphome/components/xiaomi_lywsd03mmc/* @ahpohl

View file

@ -35,11 +35,16 @@ if bashio::config.has_value 'default_compile_process_limit'; then
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit')
else
if grep -q 'Raspberry Pi 3' /proc/cpuinfo; then
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=1;
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=1
fi
fi
mkdir -p "${pio_cache_base}"
if bashio::fs.directory_exists '/config/esphome/.esphome'; then
bashio::log.info "Removing old .esphome directory..."
rm -rf /config/esphome/.esphome
fi
bashio::log.info "Starting ESPHome dashboard..."
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon

View file

@ -85,6 +85,8 @@ def choose_upload_log_host(
options = []
for port in get_serial_ports():
options.append((f"{port.path} ({port.description})", port.path))
if default == "SERIAL":
return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
options.append((f"Over The Air ({CORE.address})", CORE.address))
if default == "OTA":
@ -218,14 +220,16 @@ def compile_program(args, config):
return 0 if idedata is not None else 1
def upload_using_esptool(config, port):
def upload_using_esptool(config, port, file):
from esphome import platformio_api
first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
"upload_speed", 460800
)
def run_esptool(baud_rate):
if file is not None:
flash_images = [platformio_api.FlashImage(path=file, offset="0x0")]
else:
idedata = platformio_api.get_idedata(config)
firmware_offset = "0x10000" if CORE.is_esp32 else "0x0"
@ -242,6 +246,7 @@ def upload_using_esptool(config, port):
mcu = get_esp32_variant().lower()
def run_esptool(baud_rate):
cmd = [
"esptool.py",
"--before",
@ -292,7 +297,8 @@ def upload_using_platformio(config, port):
def upload_program(config, args, host):
if get_port_type(host) == "SERIAL":
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
return upload_using_esptool(config, host)
file = getattr(args, "file", None)
return upload_using_esptool(config, host, file)
if CORE.target_platform in (PLATFORM_RP2040):
return upload_using_platformio(config, args.device)

View file

@ -5,6 +5,10 @@ from esphome.const import CONF_ANALOG, CONF_INPUT
from esphome.core import CORE
from esphome.components.esp32 import get_esp32_variant
from esphome.const import (
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
@ -143,7 +147,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
def validate_adc_pin(value):
if str(value).upper() == "VCC":
return cv.only_on_esp8266("VCC")
return cv.only_on([PLATFORM_ESP8266, PLATFORM_RP2040])("VCC")
if str(value).upper() == "TEMPERATURE":
return cv.only_on_rp2040("TEMPERATURE")

View file

@ -12,6 +12,9 @@ ADC_MODE(ADC_VCC)
#endif
#ifdef USE_RP2040
#ifdef CYW43_USES_VSYS_PIN
#include "pico/cyw43_arch.h"
#endif
#include <hardware/adc.h>
#endif
@ -123,13 +126,19 @@ void ADCSensor::dump_config() {
}
}
#endif // USE_ESP32
#ifdef USE_RP2040
if (this->is_temperature_) {
ESP_LOGCONFIG(TAG, " Pin: Temperature");
} else {
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", pin_);
#endif // USE_ADC_SENSOR_VCC
}
#endif
#endif // USE_RP2040
LOG_UPDATE_INTERVAL(this);
}
@ -238,7 +247,20 @@ float ADCSensor::sample() {
delay(1);
adc_select_input(4);
} else {
uint8_t pin = this->pin_->get_pin();
uint8_t pin;
#ifdef USE_ADC_SENSOR_VCC
#ifdef CYW43_USES_VSYS_PIN
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
// VSYS ADC both share GPIO29
cyw43_thread_enter();
#endif // CYW43_USES_VSYS_PIN
pin = PICO_VSYS_PIN;
#else
pin = this->pin_->get_pin();
#endif // USE_ADC_SENSOR_VCC
adc_gpio_init(pin);
adc_select_input(pin - 26);
}
@ -246,11 +268,23 @@ float ADCSensor::sample() {
int32_t raw = adc_read();
if (this->is_temperature_) {
adc_set_temp_sensor_enabled(false);
} else {
#ifdef USE_ADC_SENSOR_VCC
#ifdef CYW43_USES_VSYS_PIN
cyw43_thread_exit();
#endif // CYW43_USES_VSYS_PIN
#endif // USE_ADC_SENSOR_VCC
}
if (output_raw_) {
return raw;
}
return raw * 3.3f / 4096.0f;
float coeff = 1.0;
#ifdef USE_ADC_SENSOR_VCC
// As per Raspberry Pico (W) datasheet (section 2.1) the VSYS/3 is measured
coeff = 3.0;
#endif // USE_ADC_SENSOR_VCC
return raw * 3.3f / 4096.0f * coeff;
}
#endif

View file

@ -6,6 +6,9 @@ from esphome.const import (
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
CONF_CURRENT,
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_FREQUENCY,
@ -31,10 +34,6 @@ from esphome.const import (
UNIT_WATT_HOURS,
)
CONF_PHASE_A = "phase_a"
CONF_PHASE_B = "phase_b"
CONF_PHASE_C = "phase_c"
CONF_LINE_FREQUENCY = "line_frequency"
CONF_CHIP_TEMPERATURE = "chip_temperature"
CONF_GAIN_PGA = "gain_pga"

View file

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

View file

@ -0,0 +1,270 @@
#include "bmi160.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bmi160 {
static const char *const TAG = "bmi160";
const uint8_t BMI160_REGISTER_CHIPID = 0x00;
const uint8_t BMI160_REGISTER_CMD = 0x7E;
enum class Cmd : uint8_t {
START_FOC = 0x03,
ACCL_SET_PMU_MODE = 0b00010000, // last 2 bits are mode
GYRO_SET_PMU_MODE = 0b00010100, // last 2 bits are mode
MAG_SET_PMU_MODE = 0b00011000, // last 2 bits are mode
PROG_NVM = 0xA0,
FIFO_FLUSH = 0xB0,
INT_RESET = 0xB1,
SOFT_RESET = 0xB6,
STEP_CNT_CLR = 0xB2,
};
enum class GyroPmuMode : uint8_t {
SUSPEND = 0b00,
NORMAL = 0b01,
LOW_POWER = 0b10,
};
enum class AcclPmuMode : uint8_t {
SUSPEND = 0b00,
NORMAL = 0b01,
FAST_STARTUP = 0b11,
};
enum class MagPmuMode : uint8_t {
SUSPEND = 0b00,
NORMAL = 0b01,
LOW_POWER = 0b10,
};
const uint8_t BMI160_REGISTER_ACCEL_CONFIG = 0x40;
enum class AcclFilterMode : uint8_t {
POWER_SAVING = 0b00000000,
PERF = 0b10000000,
};
enum class AcclBandwidth : uint8_t {
OSR4_AVG1 = 0b00000000,
OSR2_AVG2 = 0b00010000,
NORMAL_AVG4 = 0b00100000,
RES_AVG8 = 0b00110000,
RES_AVG16 = 0b01000000,
RES_AVG32 = 0b01010000,
RES_AVG64 = 0b01100000,
RES_AVG128 = 0b01110000,
};
enum class AccelOutputDataRate : uint8_t {
HZ_25_32 = 0b0001, // 25/32 Hz
HZ_25_16 = 0b0010, // 25/16 Hz
HZ_25_8 = 0b0011, // 25/8 Hz
HZ_25_4 = 0b0100, // 25/4 Hz
HZ_25_2 = 0b0101, // 25/2 Hz
HZ_25 = 0b0110, // 25 Hz
HZ_50 = 0b0111, // 50 Hz
HZ_100 = 0b1000, // 100 Hz
HZ_200 = 0b1001, // 200 Hz
HZ_400 = 0b1010, // 400 Hz
HZ_800 = 0b1011, // 800 Hz
HZ_1600 = 0b1100, // 1600 Hz
};
const uint8_t BMI160_REGISTER_ACCEL_RANGE = 0x41;
enum class AccelRange : uint8_t {
RANGE_2G = 0b0011,
RANGE_4G = 0b0101,
RANGE_8G = 0b1000,
RANGE_16G = 0b1100,
};
const uint8_t BMI160_REGISTER_GYRO_CONFIG = 0x42;
enum class GyroBandwidth : uint8_t {
OSR4 = 0x00,
OSR2 = 0x10,
NORMAL = 0x20,
};
enum class GyroOuputDataRate : uint8_t {
HZ_25 = 0x06,
HZ_50 = 0x07,
HZ_100 = 0x08,
HZ_200 = 0x09,
HZ_400 = 0x0A,
HZ_800 = 0x0B,
HZ_1600 = 0x0C,
HZ_3200 = 0x0D,
};
const uint8_t BMI160_REGISTER_GYRO_RANGE = 0x43;
enum class GyroRange : uint8_t {
RANGE_2000_DPS = 0x0, // ±2000 °/s
RANGE_1000_DPS = 0x1,
RANGE_500_DPS = 0x2,
RANGE_250_DPS = 0x3,
RANGE_125_DPS = 0x4,
};
const uint8_t BMI160_REGISTER_DATA_GYRO_X_LSB = 0x0C;
const uint8_t BMI160_REGISTER_DATA_GYRO_X_MSB = 0x0D;
const uint8_t BMI160_REGISTER_DATA_GYRO_Y_LSB = 0x0E;
const uint8_t BMI160_REGISTER_DATA_GYRO_Y_MSB = 0x0F;
const uint8_t BMI160_REGISTER_DATA_GYRO_Z_LSB = 0x10;
const uint8_t BMI160_REGISTER_DATA_GYRO_Z_MSB = 0x11;
const uint8_t BMI160_REGISTER_DATA_ACCEL_X_LSB = 0x12;
const uint8_t BMI160_REGISTER_DATA_ACCEL_X_MSB = 0x13;
const uint8_t BMI160_REGISTER_DATA_ACCEL_Y_LSB = 0x14;
const uint8_t BMI160_REGISTER_DATA_ACCEL_Y_MSB = 0x15;
const uint8_t BMI160_REGISTER_DATA_ACCEL_Z_LSB = 0x16;
const uint8_t BMI160_REGISTER_DATA_ACCEL_Z_MSB = 0x17;
const uint8_t BMI160_REGISTER_DATA_TEMP_LSB = 0x20;
const uint8_t BMI160_REGISTER_DATA_TEMP_MSB = 0x21;
const float GRAVITY_EARTH = 9.80665f;
void BMI160Component::internal_setup_(int stage) {
switch (stage) {
case 0:
ESP_LOGCONFIG(TAG, "Setting up BMI160...");
uint8_t chipid;
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
this->mark_failed();
return;
}
ESP_LOGV(TAG, " Bringing accelerometer out of sleep...");
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::ACCL_SET_PMU_MODE | (uint8_t) AcclPmuMode::NORMAL)) {
this->mark_failed();
return;
}
ESP_LOGV(TAG, " Waiting for accelerometer to wake up...");
// need to wait (max delay in datasheet) because we can't send commands while another is in progress
// min 5ms, 10ms
this->set_timeout(10, [this]() { this->internal_setup_(1); });
break;
case 1:
ESP_LOGV(TAG, " Bringing gyroscope out of sleep...");
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) {
this->mark_failed();
return;
}
ESP_LOGV(TAG, " Waiting for gyroscope to wake up...");
// wait between 51 & 81ms, doing 100 to be safe
this->set_timeout(10, [this]() { this->internal_setup_(2); });
break;
case 2:
ESP_LOGV(TAG, " Setting up Gyro Config...");
uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25;
ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config));
if (!this->write_byte(BMI160_REGISTER_GYRO_CONFIG, gyro_config)) {
this->mark_failed();
return;
}
ESP_LOGV(TAG, " Setting up Gyro Range...");
uint8_t gyro_range = (uint8_t) GyroRange::RANGE_2000_DPS;
ESP_LOGV(TAG, " Output gyro_range: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_range));
if (!this->write_byte(BMI160_REGISTER_GYRO_RANGE, gyro_range)) {
this->mark_failed();
return;
}
ESP_LOGV(TAG, " Setting up Accel Config...");
uint8_t accel_config =
(uint8_t) AcclFilterMode::PERF | (uint8_t) AcclBandwidth::RES_AVG16 | (uint8_t) AccelOutputDataRate::HZ_25;
ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config));
if (!this->write_byte(BMI160_REGISTER_ACCEL_CONFIG, accel_config)) {
this->mark_failed();
return;
}
ESP_LOGV(TAG, " Setting up Accel Range...");
uint8_t accel_range = (uint8_t) AccelRange::RANGE_16G;
ESP_LOGV(TAG, " Output accel_range: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_range));
if (!this->write_byte(BMI160_REGISTER_ACCEL_RANGE, accel_range)) {
this->mark_failed();
return;
}
this->setup_complete_ = true;
}
}
void BMI160Component::setup() { this->internal_setup_(0); }
void BMI160Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMI160:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with BMI160 failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Acceleration X", this->accel_x_sensor_);
LOG_SENSOR(" ", "Acceleration Y", this->accel_y_sensor_);
LOG_SENSOR(" ", "Acceleration Z", this->accel_z_sensor_);
LOG_SENSOR(" ", "Gyro X", this->gyro_x_sensor_);
LOG_SENSOR(" ", "Gyro Y", this->gyro_y_sensor_);
LOG_SENSOR(" ", "Gyro Z", this->gyro_z_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
}
i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) {
uint8_t raw_data[len * 2];
// read using read_register because we have little-endian data, and read_bytes_16 will swap it
i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true);
if (err != i2c::ERROR_OK) {
return err;
}
for (int i = 0; i < len; i++) {
value[i] = (int16_t) ((uint16_t) raw_data[i * 2] | ((uint16_t) raw_data[i * 2 + 1] << 8));
}
return err;
}
void BMI160Component::update() {
if (!this->setup_complete_) {
return;
}
ESP_LOGV(TAG, " Updating BMI160...");
int16_t data[6];
if (this->read_le_int16_(BMI160_REGISTER_DATA_GYRO_X_LSB, data, 6) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
float gyro_x = (float) data[0] / (float) INT16_MAX * 2000.f;
float gyro_y = (float) data[1] / (float) INT16_MAX * 2000.f;
float gyro_z = (float) data[2] / (float) INT16_MAX * 2000.f;
float accel_x = (float) data[3] / (float) INT16_MAX * 16 * GRAVITY_EARTH;
float accel_y = (float) data[4] / (float) INT16_MAX * 16 * GRAVITY_EARTH;
float accel_z = (float) data[5] / (float) INT16_MAX * 16 * GRAVITY_EARTH;
int16_t raw_temperature;
if (this->read_le_int16_(BMI160_REGISTER_DATA_TEMP_LSB, &raw_temperature, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
float temperature = (float) raw_temperature / (float) INT16_MAX * 64.5f + 23.f;
ESP_LOGD(TAG,
"Got accel={x=%.3f m/s², y=%.3f m/s², z=%.3f m/s²}, "
"gyro={x=%.3f °/s, y=%.3f °/s, z=%.3f °/s}, temp=%.3f°C",
accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z, temperature);
if (this->accel_x_sensor_ != nullptr)
this->accel_x_sensor_->publish_state(accel_x);
if (this->accel_y_sensor_ != nullptr)
this->accel_y_sensor_->publish_state(accel_y);
if (this->accel_z_sensor_ != nullptr)
this->accel_z_sensor_->publish_state(accel_z);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->gyro_x_sensor_ != nullptr)
this->gyro_x_sensor_->publish_state(gyro_x);
if (this->gyro_y_sensor_ != nullptr)
this->gyro_y_sensor_->publish_state(gyro_y);
if (this->gyro_z_sensor_ != nullptr)
this->gyro_z_sensor_->publish_state(gyro_z);
this->status_clear_warning();
}
float BMI160Component::get_setup_priority() const { return setup_priority::DATA; }
} // namespace bmi160
} // namespace esphome

View file

@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bmi160 {
class BMI160Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override;
void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; }
void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; }
void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_gyro_x_sensor(sensor::Sensor *gyro_x_sensor) { gyro_x_sensor_ = gyro_x_sensor; }
void set_gyro_y_sensor(sensor::Sensor *gyro_y_sensor) { gyro_y_sensor_ = gyro_y_sensor; }
void set_gyro_z_sensor(sensor::Sensor *gyro_z_sensor) { gyro_z_sensor_ = gyro_z_sensor; }
protected:
sensor::Sensor *accel_x_sensor_{nullptr};
sensor::Sensor *accel_y_sensor_{nullptr};
sensor::Sensor *accel_z_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *gyro_x_sensor_{nullptr};
sensor::Sensor *gyro_y_sensor_{nullptr};
sensor::Sensor *gyro_z_sensor_{nullptr};
void internal_setup_(int stage);
bool setup_complete_{false};
/** reads `len` 16-bit little-endian integers from the given i2c register */
i2c::ErrorCode read_le_int16_(uint8_t reg, int16_t *value, uint8_t len);
};
} // namespace bmi160
} // namespace esphome

View file

@ -0,0 +1,102 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
CONF_TEMPERATURE,
CONF_ACCELERATION_X,
CONF_ACCELERATION_Y,
CONF_ACCELERATION_Z,
CONF_GYROSCOPE_X,
CONF_GYROSCOPE_Y,
CONF_GYROSCOPE_Z,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_METER_PER_SECOND_SQUARED,
ICON_ACCELERATION_X,
ICON_ACCELERATION_Y,
ICON_ACCELERATION_Z,
ICON_GYROSCOPE_X,
ICON_GYROSCOPE_Y,
ICON_GYROSCOPE_Z,
UNIT_DEGREE_PER_SECOND,
UNIT_CELSIUS,
)
DEPENDENCIES = ["i2c"]
bmi160_ns = cg.esphome_ns.namespace("bmi160")
BMI160Component = bmi160_ns.class_(
"BMI160Component", cg.PollingComponent, i2c.I2CDevice
)
accel_schema = {
"unit_of_measurement": UNIT_METER_PER_SECOND_SQUARED,
"accuracy_decimals": 2,
"state_class": STATE_CLASS_MEASUREMENT,
}
gyro_schema = {
"unit_of_measurement": UNIT_DEGREE_PER_SECOND,
"accuracy_decimals": 2,
"state_class": STATE_CLASS_MEASUREMENT,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMI160Component),
cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema(
icon=ICON_ACCELERATION_X,
**accel_schema,
),
cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema(
icon=ICON_ACCELERATION_Y,
**accel_schema,
),
cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema(
icon=ICON_ACCELERATION_Z,
**accel_schema,
),
cv.Optional(CONF_GYROSCOPE_X): sensor.sensor_schema(
icon=ICON_GYROSCOPE_X,
**gyro_schema,
),
cv.Optional(CONF_GYROSCOPE_Y): sensor.sensor_schema(
icon=ICON_GYROSCOPE_Y,
**gyro_schema,
),
cv.Optional(CONF_GYROSCOPE_Z): sensor.sensor_schema(
icon=ICON_GYROSCOPE_Z,
**gyro_schema,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x68))
)
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)
for d in ["x", "y", "z"]:
accel_key = f"acceleration_{d}"
if accel_key in config:
sens = await sensor.new_sensor(config[accel_key])
cg.add(getattr(var, f"set_accel_{d}_sensor")(sens))
accel_key = f"gyroscope_{d}"
if accel_key in config:
sens = await sensor.new_sensor(config[accel_key])
cg.add(getattr(var, f"set_gyro_{d}_sensor")(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))

View file

@ -17,6 +17,8 @@
#include <esp32/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32C3)
#include <esp32c3/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32C6)
#include <esp32c6/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S2)
#include <esp32s2/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S3)
@ -119,6 +121,8 @@ void DebugComponent::dump_config() {
model = "ESP32";
#elif defined(USE_ESP32_VARIANT_ESP32C3)
model = "ESP32-C3";
#elif defined(USE_ESP32_VARIANT_ESP32C6)
model = "ESP32-C6";
#elif defined(USE_ESP32_VARIANT_ESP32S2)
model = "ESP32-S2";
#elif defined(USE_ESP32_VARIANT_ESP32S3)
@ -202,9 +206,11 @@ void DebugComponent::dump_config() {
case RTCWDT_SYS_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core";
break;
#if !defined(USE_ESP32_VARIANT_ESP32C6)
case INTRUSION_RESET:
reset_reason = "Intrusion Reset CPU";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32)
case TGWDT_CPU_RESET:
reset_reason = "Timer Group Reset CPU";

View file

@ -22,6 +22,7 @@ from esphome.const import (
CONF_IGNORE_EFUSE_MAC_CRC,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NAME,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
TYPE_GIT,
@ -37,6 +38,7 @@ from .const import ( # noqa
KEY_BOARD,
KEY_COMPONENTS,
KEY_ESP32,
KEY_EXTRA_BUILD_FILES,
KEY_PATH,
KEY_REF,
KEY_REFRESH,
@ -73,6 +75,8 @@ def set_core_data(config):
)
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT]
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
return config
@ -166,6 +170,24 @@ def add_idf_component(
}
def add_extra_script(stage: str, filename: str, path: str):
"""Add an extra script to the project."""
key = f"{stage}:{filename}"
if add_extra_build_file(filename, path):
cg.add_platformio_option("extra_scripts", [key])
def add_extra_build_file(filename: str, path: str) -> bool:
"""Add an extra build file to the project."""
if filename not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES][filename] = {
KEY_NAME: filename,
KEY_PATH: path,
}
return True
return False
def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO platformio/framework-arduinoespressif32 value
@ -390,7 +412,11 @@ async def to_code(config):
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
add_extra_script(
"post",
"post_build2.py",
os.path.join(os.path.dirname(__file__), "post_build.py.script"),
)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
@ -604,9 +630,15 @@ def copy_files():
ignore_dangling_symlinks=True,
)
dir = os.path.dirname(__file__)
post_build_file = os.path.join(dir, "post_build.py.script")
for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items():
if file[KEY_PATH].startswith("http"):
import requests
mkdir_p(CORE.relative_build_path(os.path.dirname(file[KEY_NAME])))
with open(CORE.relative_build_path(file[KEY_NAME]), "wb") as f:
f.write(requests.get(file[KEY_PATH], timeout=30).content)
else:
copy_file_if_changed(
post_build_file,
CORE.relative_build_path("post_build.py"),
file[KEY_PATH],
CORE.relative_build_path(file[KEY_NAME]),
)

View file

@ -10,6 +10,7 @@ KEY_REF = "ref"
KEY_REFRESH = "refresh"
KEY_PATH = "path"
KEY_SUBMODULES = "submodules"
KEY_EXTRA_BUILD_FILES = "extra_build_files"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32S2 = "ESP32S2"

View file

@ -53,7 +53,11 @@ void arch_init() {
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
#if ESP_IDF_VERSION_MAJOR >= 5
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
#else
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
#endif
uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); }
#ifdef USE_ESP_IDF

View file

@ -98,10 +98,9 @@ def validate_truetype_file(value):
def _compute_local_font_dir(name) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
h = hashlib.new("sha256")
h.update(name.encode())
return base_dir / h.hexdigest()[:8]
return Path(CORE.data_dir) / DOMAIN / h.hexdigest()[:8]
def _compute_gfonts_local_path(value) -> Path:

View file

@ -15,8 +15,14 @@ CODEOWNERS = ["@esphome/core"]
globals_ns = cg.esphome_ns.namespace("globals")
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.Component
)
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
MULTI_CONF = True
CONFIG_SCHEMA = cv.Schema(
{
@ -24,6 +30,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
).extend(cv.COMPONENT_SCHEMA)
@ -32,12 +39,19 @@ CONFIG_SCHEMA = cv.Schema(
@coroutine_with_priority(-100.0)
async def to_code(config):
type_ = cg.RawExpression(config[CONF_TYPE])
template_args = cg.TemplateArguments(type_)
restore = config[CONF_RESTORE_VALUE]
# Special casing the strings to their own class with a different save/restore mechanism
if str(type_) == "std::string" and restore:
template_args = cg.TemplateArguments(
type_, config.get(CONF_MAX_RESTORE_DATA_LENGTH, 63) + 1
)
type = RestoringGlobalStringComponent
else:
template_args = cg.TemplateArguments(type_)
type = RestoringGlobalsComponent if restore else GlobalsComponent
res_type = type.template(template_args)
res_type = type.template(template_args)
initial_value = None
if CONF_INITIAL_VALUE in config:
initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE])

View file

@ -65,6 +65,64 @@ template<typename T> class RestoringGlobalsComponent : public Component {
ESPPreferenceObject rtc_;
};
// Use with string or subclasses of strings
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalStringComponent() = default;
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
T &value() { return this->value_; }
void setup() override {
char temp[SZ];
this->rtc_ = global_preferences->make_preference<uint8_t[SZ]>(1944399030U ^ this->name_hash_);
bool hasdata = this->rtc_.load(&temp);
if (hasdata) {
this->value_.assign(temp + 1, temp[0]);
}
this->prev_value_.assign(this->value_);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }
protected:
void store_value_() {
int diff = this->value_.compare(this->prev_value_);
if (diff != 0) {
// Make it into a length prefixed thing
unsigned char temp[SZ];
// If string is bigger than the allocation, do not save it.
// We don't need to waste ram setting prev_value either.
int size = this->value_.size();
// Less than, not less than or equal, SZ includes the length byte.
if (size < SZ) {
memcpy(temp + 1, this->value_.c_str(), size);
// SZ should be pre checked at the schema level, it can't go past the char range.
temp[0] = ((unsigned char) size);
this->rtc_.save(&temp);
this->prev_value_.assign(this->value_);
}
}
}
T value_{};
T prev_value_{};
uint32_t name_hash_{};
ESPPreferenceObject rtc_;
};
template<class C, typename... Ts> class GlobalVarSetAction : public Action<Ts...> {
public:
explicit GlobalVarSetAction(C *parent) : parent_(parent) {}
@ -81,6 +139,7 @@ template<class C, typename... Ts> class GlobalVarSetAction : public Action<Ts...
template<typename T> T &id(GlobalsComponent<T> *value) { return value->value(); }
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
} // namespace globals
} // namespace esphome

View file

@ -6,6 +6,9 @@ from esphome.const import (
CONF_CURRENT,
CONF_FREQUENCY,
CONF_ID,
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
@ -21,10 +24,6 @@ from esphome.const import (
UNIT_WATT,
)
CONF_PHASE_A = "phase_a"
CONF_PHASE_B = "phase_b"
CONF_PHASE_C = "phase_c"
CONF_ENERGY_PRODUCTION_DAY = "energy_production_day"
CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production"
CONF_TOTAL_GENERATION_TIME = "total_generation_time"

View file

@ -6,6 +6,9 @@ from esphome.const import (
CONF_CURRENT,
CONF_FREQUENCY,
CONF_ID,
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
@ -24,9 +27,6 @@ from esphome.const import (
UNIT_WATT,
)
CONF_PHASE_A = "phase_a"
CONF_PHASE_B = "phase_b"
CONF_PHASE_C = "phase_c"
CONF_ENERGY_PRODUCTION_DAY = "energy_production_day"
CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production"
CONF_TOTAL_GENERATION_TIME = "total_generation_time"

View file

@ -52,7 +52,7 @@ Image_ = image_ns.class_("Image")
def _compute_local_icon_path(value) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN / "mdi"
base_dir = Path(CORE.data_dir) / DOMAIN / "mdi"
return base_dir / f"{value[CONF_ICON]}.svg"

View file

@ -168,9 +168,9 @@ def _notify_old_style(config):
# NOTE: Keep this in mind when updating the recommended version:
# * For all constants below, update platformio.ini (in this repo)
ARDUINO_VERSIONS = {
"dev": (cv.Version(0, 0, 0), "https://github.com/kuba2k2/libretiny.git"),
"dev": (cv.Version(0, 0, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(0, 0, 0), None),
"recommended": (cv.Version(1, 3, 0), None),
"recommended": (cv.Version(1, 4, 0), None),
}

View file

@ -54,7 +54,7 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) {
bool decode_mifare_classic_tlv(std::vector<uint8_t> &data, uint32_t &message_length, uint8_t &message_start_index) {
uint8_t i = get_mifare_classic_ndef_start_index(data);
if (i < 0 || data[i] != 0x03) {
if (data[i] != 0x03) {
ESP_LOGE(TAG, "Error, Can't decode message length.");
return false;
}

View file

@ -7,6 +7,7 @@
#include "esphome/components/nfc/nfc.h"
#include "esphome/components/nfc/automation.h"
#include <cinttypes>
#include <vector>
namespace esphome {
@ -74,10 +75,11 @@ class PN532 : public PollingComponent {
bool write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(std::vector<uint8_t> &uid);
bool read_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &data);
bool is_mifare_ultralight_formatted_();
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index);
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
bool write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message);
bool clean_mifare_ultralight_();

View file

@ -9,93 +9,104 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_ultralight";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(std::vector<uint8_t> &uid) {
if (!this->is_mifare_ultralight_formatted_()) {
ESP_LOGD(TAG, "Not NDEF formatted");
std::vector<uint8_t> data;
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
data)) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
if (!this->is_mifare_ultralight_formatted_(data)) {
ESP_LOGW(TAG, "Not NDEF formatted");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
uint8_t message_length;
uint8_t message_start_index;
if (!this->find_mifare_ultralight_ndef_(message_length, message_start_index)) {
if (!this->find_mifare_ultralight_ndef_(data, message_length, message_start_index)) {
ESP_LOGW(TAG, "Couldn't find NDEF message");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
ESP_LOGVV(TAG, "message length: %d, start: %d", message_length, message_start_index);
ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index);
if (message_length == 0) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
std::vector<uint8_t> data;
for (uint8_t page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; page < nfc::MIFARE_ULTRALIGHT_MAX_PAGE; page++) {
std::vector<uint8_t> page_data;
if (!this->read_mifare_ultralight_page_(page, page_data)) {
ESP_LOGE(TAG, "Error reading page %d", page);
// we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages
const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0;
if (read_length) {
if (!read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data)) {
ESP_LOGE(TAG, "Error reading tag data");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
data.insert(data.end(), page_data.begin(), page_data.end());
if (data.size() >= (message_length + message_start_index))
break;
}
data.erase(data.begin(), data.begin() + message_start_index);
data.erase(data.begin() + message_length, data.end());
// we need to trim off page 3 as well as any bytes ahead of message_start_index
data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2, data);
}
bool PN532::read_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &data) {
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data) {
const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
std::vector<uint8_t> response;
for (uint8_t i = 0; i * read_increment < num_bytes; i++) {
if (!this->write_command_({
PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card
nfc::MIFARE_CMD_READ,
page_num,
uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page),
})) {
return false;
}
if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) {
if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response[0] != 0x00) {
return false;
}
data.erase(data.begin());
// We only want 1 page of data but the PN532 returns 4 at once.
data.erase(data.begin() + 4, data.end());
uint16_t bytes_offset = (i + 1) * read_increment;
auto pages_in_end_itr = bytes_offset <= num_bytes ? response.end() : response.end() - (bytes_offset - num_bytes);
ESP_LOGVV(TAG, "Pages %d-%d: %s", page_num, page_num + 4, nfc::format_bytes(data).c_str());
if ((pages_in_end_itr > response.begin()) && (pages_in_end_itr <= response.end())) {
data.insert(data.end(), response.begin() + 1, pages_in_end_itr);
}
}
ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str());
return true;
}
bool PN532::is_mifare_ultralight_formatted_() {
std::vector<uint8_t> data;
if (this->read_mifare_ultralight_page_(4, data)) {
return !(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[3] == 0xFF);
}
return true;
bool PN532::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6) {
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
return (page_3_to_6.size() > p4_offset + 3) &&
!((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) &&
(page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF));
}
uint16_t PN532::read_mifare_ultralight_capacity_() {
std::vector<uint8_t> data;
if (this->read_mifare_ultralight_page_(3, data)) {
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data)) {
ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
return data[2] * 8U;
}
return 0;
}
bool PN532::find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index) {
std::vector<uint8_t> data;
for (int page = 4; page < 6; page++) {
std::vector<uint8_t> page_data;
if (!this->read_mifare_ultralight_page_(page, page_data)) {
bool PN532::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index) {
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
if (!(page_3_to_6.size() > p4_offset + 5)) {
return false;
}
data.insert(data.end(), page_data.begin(), page_data.end());
}
if (data[0] == 0x03) {
message_length = data[1];
if (page_3_to_6[p4_offset + 0] == 0x03) {
message_length = page_3_to_6[p4_offset + 1];
message_start_index = 2;
return true;
} else if (data[5] == 0x03) {
message_length = data[6];
} else if (page_3_to_6[p4_offset + 5] == 0x03) {
message_length = page_3_to_6[p4_offset + 6];
message_start_index = 7;
return true;
}
@ -111,7 +122,7 @@ bool PN532::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMes
uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length);
if (buffer_length > capacity) {
ESP_LOGE(TAG, "Message length exceeds tag capacity %d > %d", buffer_length, capacity);
ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity);
return false;
}
@ -164,13 +175,13 @@ bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t>
});
data.insert(data.end(), write_data.begin(), write_data.end());
if (!this->write_command_(data)) {
ESP_LOGE(TAG, "Error writing page %d", page_num);
ESP_LOGE(TAG, "Error writing page %u", page_num);
return false;
}
std::vector<uint8_t> response;
if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response)) {
ESP_LOGE(TAG, "Error writing page %d", page_num);
ESP_LOGE(TAG, "Error writing page %u", page_num);
return false;
}

View file

@ -87,12 +87,7 @@ def get_firmware(value):
url = value[CONF_URL]
if CONF_SHA256 in value: # we have a hash, enable caching
path = (
Path(CORE.config_dir)
/ ".esphome"
/ DOMAIN
/ (value[CONF_SHA256] + "_fw_stm.bin")
)
path = Path(CORE.data_dir) / DOMAIN / (value[CONF_SHA256] + "_fw_stm.bin")
if not path.is_file():
firmware_data, dl_hash = dl(url)

View file

@ -1,6 +1,17 @@
import re
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components.esp32.const import (
KEY_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
)
from esphome import pins
from esphome.const import (
CONF_CLK_PIN,
@ -9,6 +20,11 @@ from esphome.const import (
CONF_MOSI_PIN,
CONF_SPI_ID,
CONF_CS_PIN,
CONF_NUMBER,
CONF_INVERTED,
KEY_CORE,
KEY_TARGET_PLATFORM,
KEY_VARIANT,
)
from esphome.core import coroutine_with_priority, CORE
@ -34,10 +50,147 @@ SPI_DATA_RATE_OPTIONS = {
}
SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS))
MULTI_CONF = True
CONF_FORCE_SW = "force_sw"
CONF_INTERFACE = "interface"
CONF_INTERFACE_INDEX = "interface_index"
CONFIG_SCHEMA = cv.All(
def get_target_platform():
return (
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM]
if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE]
else ""
)
def get_target_variant():
return (
CORE.data[KEY_ESP32][KEY_VARIANT] if KEY_VARIANT in CORE.data[KEY_ESP32] else ""
)
# Get a list of available hardware interfaces based on target and variant.
# The returned value is a list of lists of names
def get_hw_interface_list():
target_platform = get_target_platform()
if target_platform == "esp8266":
return [["spi", "hspi"]]
if target_platform == "esp32":
if get_target_variant() in [
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
]:
return [["spi", "spi2"]]
return [["spi", "spi2"], ["spi3"]]
if target_platform == "rp2040":
return [["spi"]]
return []
# Given an SPI name, return the index of it in the available list
def get_spi_index(name):
for i, ilist in enumerate(get_hw_interface_list()):
if name in ilist:
return i
# Should never get to here.
raise cv.Invalid(f"{name} is not an available SPI")
# Check that pins are suitable for HW spi
# TODO verify that the pins are internal
def validate_hw_pins(spi):
clk_pin = spi[CONF_CLK_PIN]
if clk_pin[CONF_INVERTED]:
return False
clk_pin_no = clk_pin[CONF_NUMBER]
sdo_pin_no = -1
sdi_pin_no = -1
if CONF_MOSI_PIN in spi:
sdo_pin = spi[CONF_MOSI_PIN]
if sdo_pin[CONF_INVERTED]:
return False
sdo_pin_no = sdo_pin[CONF_NUMBER]
if CONF_MISO_PIN in spi:
sdi_pin = spi[CONF_MISO_PIN]
if sdi_pin[CONF_INVERTED]:
return False
sdi_pin_no = sdi_pin[CONF_NUMBER]
target_platform = get_target_platform()
if target_platform == "esp8266":
if clk_pin_no == 6:
return sdo_pin_no in (-1, 8) and sdi_pin_no in (-1, 7)
if clk_pin_no == 14:
return sdo_pin_no in (-1, 13) and sdi_pin_no in (-1, 12)
return False
if target_platform == "esp32":
return clk_pin_no >= 0
return False
def validate_spi_config(config):
available = list(range(len(get_hw_interface_list())))
for spi in config:
interface = spi[CONF_INTERFACE]
if spi[CONF_FORCE_SW]:
if interface == "any":
spi[CONF_INTERFACE] = interface = "software"
elif interface != "software":
raise cv.Invalid("force_sw is deprecated - use interface: software")
if interface == "software":
pass
elif interface == "any":
if not validate_hw_pins(spi):
spi[CONF_INTERFACE] = "software"
elif interface == "hardware":
if len(available) == 0:
raise cv.Invalid("No hardware interface available")
index = spi[CONF_INTERFACE_INDEX] = available[0]
available.remove(index)
else:
# Must be a specific name
index = spi[CONF_INTERFACE_INDEX] = get_spi_index(interface)
if index not in available:
raise cv.Invalid(
f"interface '{interface}' not available here (may be already assigned)"
)
available.remove(index)
# Second time around:
# Any specific names and any 'hardware' requests will have already been filled,
# so just need to assign remaining hardware to 'any' requests.
for spi in config:
if spi[CONF_INTERFACE] == "any" and len(available) != 0:
index = available[0]
spi[CONF_INTERFACE_INDEX] = index
available.remove(index)
if CONF_INTERFACE_INDEX in spi and not validate_hw_pins(spi):
raise cv.Invalid("Invalid pin selections for hardware SPI interface")
return config
# Given an SPI index, convert to a string that represents the C++ object for it.
def get_spi_interface(index):
if CORE.using_esp_idf:
return ["SPI2_HOST", "SPI3_HOST"][index]
# Arduino code follows
platform = get_target_platform()
if platform == "rp2040":
return "&spi1"
if index == 0:
return "&SPI"
# Following code can't apply to C2, H2 or 8266 since they have only one SPI
if get_target_variant() in (VARIANT_ESP32S3, VARIANT_ESP32S2):
return "new SPIClass(FSPI)"
return "return new SPIClass(HSPI)"
SPI_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SPIComponent),
@ -45,28 +198,47 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_FORCE_SW, default=False): cv.boolean,
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of(
*sum(get_hw_interface_list(), ["software", "hardware", "any"]),
lower=True,
),
}
),
cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN),
cv.only_on(["esp32", "esp8266", "rp2040"]),
)
CONFIG_SCHEMA = cv.All(
cv.ensure_list(SPI_SCHEMA),
validate_spi_config,
)
@coroutine_with_priority(1.0)
async def to_code(config):
async def to_code(configs):
cg.add_global(spi_ns.using)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for spi in configs:
var = cg.new_Pvariable(spi[CONF_ID])
await cg.register_component(var, spi)
clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN])
clk = await cg.gpio_pin_expression(spi[CONF_CLK_PIN])
cg.add(var.set_clk(clk))
cg.add(var.set_force_sw(config[CONF_FORCE_SW]))
if CONF_MISO_PIN in config:
miso = await cg.gpio_pin_expression(config[CONF_MISO_PIN])
if CONF_MISO_PIN in spi:
miso = await cg.gpio_pin_expression(spi[CONF_MISO_PIN])
cg.add(var.set_miso(miso))
if CONF_MOSI_PIN in config:
mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN])
if CONF_MOSI_PIN in spi:
mosi = await cg.gpio_pin_expression(spi[CONF_MOSI_PIN])
cg.add(var.set_mosi(mosi))
if CONF_INTERFACE_INDEX in spi:
index = spi[CONF_INTERFACE_INDEX]
cg.add(var.set_interface(cg.RawExpression(get_spi_interface(index))))
cg.add(
var.set_interface_name(
re.sub(
r"\W", "", get_spi_interface(index).replace("new SPIClass", "")
)
)
)
if CORE.using_arduino:
cg.add_library("SPI", None)

View file

@ -1,268 +1,116 @@
#include "spi.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/application.h"
namespace esphome {
namespace spi {
static const char *const TAG = "spi";
const char *const TAG = "spi";
void IRAM_ATTR HOT SPIComponent::disable() {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
this->hw_spi_->endTransaction();
}
#endif // USE_SPI_ARDUINO_BACKEND
if (this->active_cs_) {
this->active_cs_->digital_write(true);
this->active_cs_ = nullptr;
SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
new SPIDelegateDummy();
// https://bugs.llvm.org/show_bug.cgi?id=48040
bool SPIDelegate::is_ready() { return true; }
GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate,
GPIOPin *cs_pin) {
if (this->devices_.count(device) != 0) {
ESP_LOGE(TAG, "SPI device already registered");
return this->devices_[device];
}
SPIDelegate *delegate = this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin); // NOLINT
this->devices_[device] = delegate;
return delegate;
}
void SPIComponent::unregister_device(SPIClient *device) {
if (this->devices_.count(device) == 0) {
esph_log_e(TAG, "SPI device not registered");
return;
}
delete this->devices_[device]; // NOLINT
this->devices_.erase(device);
}
void SPIComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up SPI bus...");
this->clk_->setup();
this->clk_->digital_write(true);
ESP_LOGD(TAG, "Setting up SPI bus...");
#ifdef USE_SPI_ARDUINO_BACKEND
bool use_hw_spi = !this->force_sw_;
const bool has_miso = this->miso_ != nullptr;
const bool has_mosi = this->mosi_ != nullptr;
int8_t clk_pin = -1, miso_pin = -1, mosi_pin = -1;
if (!this->clk_->is_internal())
use_hw_spi = false;
if (has_miso && !miso_->is_internal())
use_hw_spi = false;
if (has_mosi && !mosi_->is_internal())
use_hw_spi = false;
if (use_hw_spi) {
auto *clk_internal = (InternalGPIOPin *) clk_;
auto *miso_internal = (InternalGPIOPin *) miso_;
auto *mosi_internal = (InternalGPIOPin *) mosi_;
if (clk_internal->is_inverted())
use_hw_spi = false;
if (has_miso && miso_internal->is_inverted())
use_hw_spi = false;
if (has_mosi && mosi_internal->is_inverted())
use_hw_spi = false;
if (use_hw_spi) {
clk_pin = clk_internal->get_pin();
miso_pin = has_miso ? miso_internal->get_pin() : -1;
mosi_pin = has_mosi ? mosi_internal->get_pin() : -1;
}
}
#ifdef USE_ESP8266
if (!(clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) &&
!(clk_pin == 14 && (!has_miso || miso_pin == 12) && (!has_mosi || mosi_pin == 13)))
use_hw_spi = false;
if (use_hw_spi) {
this->hw_spi_ = &SPI;
this->hw_spi_->pins(clk_pin, miso_pin, mosi_pin, 0);
this->hw_spi_->begin();
if (this->sdo_pin_ == nullptr)
this->sdo_pin_ = NullPin::NULL_PIN;
if (this->sdi_pin_ == nullptr)
this->sdi_pin_ = NullPin::NULL_PIN;
if (this->clk_pin_ == nullptr) {
ESP_LOGE(TAG, "No clock pin for SPI");
this->mark_failed();
return;
}
#endif // USE_ESP8266
#ifdef USE_ESP32
static uint8_t spi_bus_num = 0;
if (spi_bus_num >= 2) {
use_hw_spi = false;
}
if (use_hw_spi) {
if (spi_bus_num == 0) {
this->hw_spi_ = &SPI;
if (this->using_hw_) {
this->spi_bus_ = SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_);
if (this->spi_bus_ == nullptr) {
ESP_LOGE(TAG, "Unable to allocate SPI interface");
this->mark_failed();
}
} else {
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C6)
this->hw_spi_ = new SPIClass(FSPI); // NOLINT(cppcoreguidelines-owning-memory)
#else
this->hw_spi_ = new SPIClass(HSPI); // NOLINT(cppcoreguidelines-owning-memory)
#endif // USE_ESP32_VARIANT
}
spi_bus_num++;
this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin);
return;
}
#endif // USE_ESP32
#ifdef USE_RP2040
static uint8_t spi_bus_num = 0;
if (spi_bus_num >= 2) {
use_hw_spi = false;
}
if (use_hw_spi) {
SPIClassRP2040 *spi;
if (spi_bus_num == 0) {
spi = &SPI;
} else {
spi = &SPI1;
}
spi_bus_num++;
if (miso_pin != -1)
spi->setRX(miso_pin);
if (mosi_pin != -1)
spi->setTX(mosi_pin);
spi->setSCK(clk_pin);
this->hw_spi_ = spi;
this->hw_spi_->begin();
return;
}
#endif // USE_RP2040
#endif // USE_SPI_ARDUINO_BACKEND
if (this->miso_ != nullptr) {
this->miso_->setup();
}
if (this->mosi_ != nullptr) {
this->mosi_->setup();
this->mosi_->digital_write(false);
this->spi_bus_ = new SPIBus(this->clk_pin_, this->sdo_pin_, this->sdi_pin_); // NOLINT
this->clk_pin_->setup();
this->clk_pin_->digital_write(true);
this->sdo_pin_->setup();
this->sdi_pin_->setup();
}
}
void SPIComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SPI bus:");
LOG_PIN(" CLK Pin: ", this->clk_);
LOG_PIN(" MISO Pin: ", this->miso_);
LOG_PIN(" MOSI Pin: ", this->mosi_);
#ifdef USE_SPI_ARDUINO_BACKEND
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", YESNO(this->hw_spi_ != nullptr));
#endif // USE_SPI_ARDUINO_BACKEND
}
float SPIComponent::get_setup_priority() const { return setup_priority::BUS; }
void SPIComponent::cycle_clock_(bool value) {
uint32_t start = arch_get_cpu_cycle_count();
while (start - arch_get_cpu_cycle_count() < this->wait_cycle_)
;
this->clk_->digital_write(value);
start += this->wait_cycle_;
while (start - arch_get_cpu_cycle_count() < this->wait_cycle_)
;
LOG_PIN(" CLK Pin: ", this->clk_pin_)
LOG_PIN(" SDI Pin: ", this->sdi_pin_)
LOG_PIN(" SDO Pin: ", this->sdo_pin_)
if (this->spi_bus_->is_hw()) {
ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_);
} else {
ESP_LOGCONFIG(TAG, " Using software SPI");
}
}
// NOLINTNEXTLINE
#ifndef CLANG_TIDY
#pragma GCC optimize("unroll-loops")
// NOLINTNEXTLINE
#pragma GCC optimize("O2")
#endif // CLANG_TIDY
void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); }
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE>
uint8_t HOT SPIComponent::transfer_(uint8_t data) {
uint8_t SPIDelegateBitBash::transfer(uint8_t data) {
// Clock starts out at idle level
this->clk_->digital_write(CLOCK_POLARITY);
this->clk_pin_->digital_write(clock_polarity_);
uint8_t out_data = 0;
for (uint8_t i = 0; i < 8; i++) {
uint8_t shift;
if (BIT_ORDER == BIT_ORDER_MSB_FIRST) {
if (bit_order_ == BIT_ORDER_MSB_FIRST) {
shift = 7 - i;
} else {
shift = i;
}
if (CLOCK_PHASE == CLOCK_PHASE_LEADING) {
if (clock_phase_ == CLOCK_PHASE_LEADING) {
// sampling on leading edge
if (WRITE) {
this->mosi_->digital_write(data & (1 << shift));
}
// SAMPLE!
this->cycle_clock_(!CLOCK_POLARITY);
if (READ) {
out_data |= uint8_t(this->miso_->digital_read()) << shift;
}
this->cycle_clock_(CLOCK_POLARITY);
this->sdo_pin_->digital_write(data & (1 << shift));
this->cycle_clock_();
out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift;
this->clk_pin_->digital_write(!this->clock_polarity_);
this->cycle_clock_();
this->clk_pin_->digital_write(this->clock_polarity_);
} else {
// sampling on trailing edge
this->cycle_clock_(!CLOCK_POLARITY);
if (WRITE) {
this->mosi_->digital_write(data & (1 << shift));
}
// SAMPLE!
this->cycle_clock_(CLOCK_POLARITY);
if (READ) {
out_data |= uint8_t(this->miso_->digital_read()) << shift;
this->cycle_clock_();
this->clk_pin_->digital_write(!this->clock_polarity_);
this->sdo_pin_->digital_write(data & (1 << shift));
this->cycle_clock_();
out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift;
this->clk_pin_->digital_write(this->clock_polarity_);
}
}
}
App.feed_wdt();
return out_data;
}
// Generate with (py3):
//
// from itertools import product
// bit_orders = ['BIT_ORDER_LSB_FIRST', 'BIT_ORDER_MSB_FIRST']
// clock_pols = ['CLOCK_POLARITY_LOW', 'CLOCK_POLARITY_HIGH']
// clock_phases = ['CLOCK_PHASE_LEADING', 'CLOCK_PHASE_TRAILING']
// reads = [False, True]
// writes = [False, True]
// cpp_bool = {False: 'false', True: 'true'}
// for b, cpol, cph, r, w in product(bit_orders, clock_pols, clock_phases, reads, writes):
// if not r and not w:
// continue
// print(f"template uint8_t SPIComponent::transfer_<{b}, {cpol}, {cph}, {cpp_bool[r]}, {cpp_bool[w]}>(uint8_t
// data);")
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>(
uint8_t data);
template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>(
uint8_t data);
} // namespace spi
} // namespace esphome

View file

@ -2,16 +2,34 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <vector>
#include <map>
#ifdef USE_ARDUINO
#define USE_SPI_ARDUINO_BACKEND
#endif
#ifdef USE_SPI_ARDUINO_BACKEND
#include <SPI.h>
#ifdef USE_RP2040
using SPIInterface = SPIClassRP2040 *;
#else
using SPIInterface = SPIClass *;
#endif
#endif
#ifdef USE_ESP_IDF
#include "driver/spi_master.h"
using SPIInterface = spi_host_device_t;
#endif // USE_ESP_IDF
/**
* Implementation of SPI Controller mode.
*/
namespace esphome {
namespace spi {
@ -48,10 +66,19 @@ enum SPIClockPhase {
/// The data is sampled on a trailing clock edge. (CPHA=1)
CLOCK_PHASE_TRAILING,
};
/** The SPI clock signal data rate. This defines for what duration the clock signal is HIGH/LOW.
* So effectively the rate of bytes can be calculated using
/**
* Modes mapping to clock phase and polarity.
*
* effective_byte_rate = spi_data_rate / 16
*/
enum SPIMode {
MODE0 = 0,
MODE1 = 1,
MODE2 = 2,
MODE3 = 3,
};
/** The SPI clock signal frequency, which determines the transfer bit rate/second.
*
* Implementations can use the pre-defined constants here, or use an integer in the template definition
* to manually use a specific data rate.
@ -71,270 +98,340 @@ enum SPIDataRate : uint32_t {
DATA_RATE_80MHZ = 80000000,
};
class SPIComponent : public Component {
/**
* A pin to replace those that don't exist.
*/
class NullPin : public GPIOPin {
friend class SPIComponent;
friend class SPIDelegate;
friend class Utility;
public:
void set_clk(GPIOPin *clk) { clk_ = clk; }
void set_miso(GPIOPin *miso) { miso_ = miso; }
void set_mosi(GPIOPin *mosi) { mosi_ = mosi; }
void set_force_sw(bool force_sw) { force_sw_ = force_sw; }
void setup() override {}
void setup() override;
void pin_mode(gpio::Flags flags) override {}
void dump_config() override;
bool digital_read() override { return false; }
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> uint8_t read_byte() {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
return this->hw_spi_->transfer(0x00);
}
#endif // USE_SPI_ARDUINO_BACKEND
return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, false>(0x00);
}
void digital_write(bool value) override {}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
void read_array(uint8_t *data, size_t length) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
this->hw_spi_->transfer(data, length);
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
for (size_t i = 0; i < length; i++) {
data[i] = this->read_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>();
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
void write_byte(uint8_t data) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
#ifdef USE_RP2040
this->hw_spi_->transfer(data);
#else
this->hw_spi_->write(data);
#endif
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, false, true>(data);
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
void write_byte16(const uint16_t data) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
#ifdef USE_RP2040
this->hw_spi_->transfer16(data);
#else
this->hw_spi_->write16(data);
#endif
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data >> 8);
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
void write_array16(const uint16_t *data, size_t length) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
for (size_t i = 0; i < length; i++) {
#ifdef USE_RP2040
this->hw_spi_->transfer16(data[i]);
#else
this->hw_spi_->write16(data[i]);
#endif
}
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
for (size_t i = 0; i < length; i++) {
this->write_byte16<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
void write_array(const uint8_t *data, size_t length) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
auto *data_c = const_cast<uint8_t *>(data);
#ifdef USE_RP2040
this->hw_spi_->transfer(data_c, length);
#else
this->hw_spi_->writeBytes(data_c, length);
#endif
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
for (size_t i = 0; i < length; i++) {
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
uint8_t transfer_byte(uint8_t data) {
if (this->miso_ != nullptr) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
return this->hw_spi_->transfer(data);
} else {
#endif // USE_SPI_ARDUINO_BACKEND
return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data);
#ifdef USE_SPI_ARDUINO_BACKEND
}
#endif // USE_SPI_ARDUINO_BACKEND
}
this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
return 0;
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
void transfer_array(uint8_t *data, size_t length) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
if (this->miso_ != nullptr) {
this->hw_spi_->transfer(data, length);
} else {
#ifdef USE_RP2040
this->hw_spi_->transfer(data, length);
#else
this->hw_spi_->writeBytes(data, length);
#endif
}
return;
}
#endif // USE_SPI_ARDUINO_BACKEND
if (this->miso_ != nullptr) {
for (size_t i = 0; i < length; i++) {
data[i] = this->transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
}
} else {
this->write_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
}
}
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, uint32_t DATA_RATE>
void enable(GPIOPin *cs) {
#ifdef USE_SPI_ARDUINO_BACKEND
if (this->hw_spi_ != nullptr) {
uint8_t data_mode = SPI_MODE0;
if (!CLOCK_POLARITY && CLOCK_PHASE) {
data_mode = SPI_MODE1;
} else if (CLOCK_POLARITY && !CLOCK_PHASE) {
data_mode = SPI_MODE2;
} else if (CLOCK_POLARITY && CLOCK_PHASE) {
data_mode = SPI_MODE3;
}
#ifdef USE_RP2040
SPISettings settings(DATA_RATE, static_cast<BitOrder>(BIT_ORDER), data_mode);
#else
SPISettings settings(DATA_RATE, BIT_ORDER, data_mode);
#endif
this->hw_spi_->beginTransaction(settings);
} else {
#endif // USE_SPI_ARDUINO_BACKEND
this->clk_->digital_write(CLOCK_POLARITY);
uint32_t cpu_freq_hz = arch_get_cpu_freq_hz();
this->wait_cycle_ = uint32_t(cpu_freq_hz) / DATA_RATE / 2ULL;
#ifdef USE_SPI_ARDUINO_BACKEND
}
#endif // USE_SPI_ARDUINO_BACKEND
if (cs != nullptr) {
this->active_cs_ = cs;
this->active_cs_->digital_write(false);
}
}
void disable();
float get_setup_priority() const override;
std::string dump_summary() const override { return std::string(); }
protected:
inline void cycle_clock_(bool value);
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE>
uint8_t transfer_(uint8_t data);
GPIOPin *clk_;
GPIOPin *miso_{nullptr};
GPIOPin *mosi_{nullptr};
GPIOPin *active_cs_{nullptr};
bool force_sw_{false};
#ifdef USE_SPI_ARDUINO_BACKEND
SPIClass *hw_spi_{nullptr};
#endif // USE_SPI_ARDUINO_BACKEND
uint32_t wait_cycle_;
static GPIOPin *const NULL_PIN; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// https://bugs.llvm.org/show_bug.cgi?id=48040
};
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, SPIDataRate DATA_RATE>
class SPIDevice {
class Utility {
public:
SPIDevice() = default;
SPIDevice(SPIComponent *parent, GPIOPin *cs) : parent_(parent), cs_(cs) {}
static int get_pin_no(GPIOPin *pin) {
if (pin == nullptr || !pin->is_internal())
return -1;
if (((InternalGPIOPin *) pin)->is_inverted())
return -1;
return ((InternalGPIOPin *) pin)->get_pin();
}
void set_spi_parent(SPIComponent *parent) { parent_ = parent; }
void set_cs_pin(GPIOPin *cs) { cs_ = cs; }
static SPIMode get_mode(SPIClockPolarity polarity, SPIClockPhase phase) {
if (polarity == CLOCK_POLARITY_HIGH) {
return phase == CLOCK_PHASE_LEADING ? MODE2 : MODE3;
}
return phase == CLOCK_PHASE_LEADING ? MODE0 : MODE1;
}
void spi_setup() {
if (this->cs_) {
this->cs_->setup();
this->cs_->digital_write(true);
static SPIClockPhase get_phase(SPIMode mode) {
switch (mode) {
case MODE0:
case MODE2:
return CLOCK_PHASE_LEADING;
default:
return CLOCK_PHASE_TRAILING;
}
}
void enable() { this->parent_->template enable<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, DATA_RATE>(this->cs_); }
static SPIClockPolarity get_polarity(SPIMode mode) {
switch (mode) {
case MODE0:
case MODE1:
return CLOCK_POLARITY_LOW;
default:
return CLOCK_POLARITY_HIGH;
}
}
};
void disable() { this->parent_->disable(); }
class SPIDelegateDummy;
uint8_t read_byte() { return this->parent_->template read_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(); }
// represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is
// a thin wrapper over SPIClass.
class SPIDelegate {
friend class SPIClient;
void read_array(uint8_t *data, size_t length) {
return this->parent_->template read_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
public:
SPIDelegate() = default;
SPIDelegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin)
: bit_order_(bit_order), data_rate_(data_rate), mode_(mode), cs_pin_(cs_pin) {
if (this->cs_pin_ == nullptr)
this->cs_pin_ = NullPin::NULL_PIN;
this->cs_pin_->setup();
this->cs_pin_->digital_write(true);
}
template<size_t N> std::array<uint8_t, N> read_array() {
std::array<uint8_t, N> data;
this->read_array(data.data(), N);
return data;
virtual ~SPIDelegate(){};
// enable CS if configured.
virtual void begin_transaction() { this->cs_pin_->digital_write(false); }
// end the transaction
virtual void end_transaction() { this->cs_pin_->digital_write(true); }
// transfer one byte, return the byte that was read.
virtual uint8_t transfer(uint8_t data) = 0;
// transfer a buffer, replace the contents with read data
virtual void transfer(uint8_t *ptr, size_t length) { this->transfer(ptr, ptr, length); }
virtual void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) {
for (size_t i = 0; i != length; i++)
rxbuf[i] = this->transfer(txbuf[i]);
}
void write_byte(uint8_t data) {
return this->parent_->template write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
// write 16 bits
virtual void write16(uint16_t data) {
if (this->bit_order_ == BIT_ORDER_MSB_FIRST) {
uint16_t buffer;
buffer = (data >> 8) | (data << 8);
this->write_array(reinterpret_cast<const uint8_t *>(&buffer), 2);
} else {
this->write_array(reinterpret_cast<const uint8_t *>(&data), 2);
}
}
void write_byte16(uint16_t data) {
return this->parent_->template write_byte16<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
virtual void write_array16(const uint16_t *data, size_t length) {
for (size_t i = 0; i != length; i++) {
this->write16(data[i]);
}
}
void write_array16(const uint16_t *data, size_t length) {
this->parent_->template write_array16<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
// write the contents of a buffer, ignore read data (buffer is unchanged.)
virtual void write_array(const uint8_t *ptr, size_t length) {
for (size_t i = 0; i != length; i++)
this->transfer(ptr[i]);
}
void write_array(const uint8_t *data, size_t length) {
this->parent_->template write_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
// read into a buffer, write nulls
virtual void read_array(uint8_t *ptr, size_t length) {
for (size_t i = 0; i != length; i++)
ptr[i] = this->transfer(0);
}
// check if device is ready
virtual bool is_ready();
protected:
SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST};
uint32_t data_rate_{1000000};
SPIMode mode_{MODE0};
GPIOPin *cs_pin_{NullPin::NULL_PIN};
static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
};
/**
* A dummy SPIDelegate that complains if it's used.
*/
class SPIDelegateDummy : public SPIDelegate {
public:
SPIDelegateDummy() = default;
uint8_t transfer(uint8_t data) override { return 0; }
void begin_transaction() override;
};
/**
* An implementation of SPI that relies only on software toggling of pins.
*
*/
class SPIDelegateBitBash : public SPIDelegate {
public:
SPIDelegateBitBash(uint32_t clock, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, GPIOPin *clk_pin,
GPIOPin *sdo_pin, GPIOPin *sdi_pin)
: SPIDelegate(clock, bit_order, mode, cs_pin), clk_pin_(clk_pin), sdo_pin_(sdo_pin), sdi_pin_(sdi_pin) {
// this calculation is pretty meaningless except at very low bit rates.
this->wait_cycle_ = uint32_t(arch_get_cpu_freq_hz()) / this->data_rate_ / 2ULL;
this->clock_polarity_ = Utility::get_polarity(this->mode_);
this->clock_phase_ = Utility::get_phase(this->mode_);
}
uint8_t transfer(uint8_t data) override;
protected:
GPIOPin *clk_pin_;
GPIOPin *sdo_pin_;
GPIOPin *sdi_pin_;
uint32_t last_transition_{0};
uint32_t wait_cycle_;
SPIClockPolarity clock_polarity_;
SPIClockPhase clock_phase_;
void HOT cycle_clock_() {
while (this->last_transition_ - arch_get_cpu_cycle_count() < this->wait_cycle_)
continue;
this->last_transition_ += this->wait_cycle_;
}
};
class SPIBus {
public:
SPIBus() = default;
SPIBus(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) : clk_pin_(clk), sdo_pin_(sdo), sdi_pin_(sdi) {}
virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) {
return new SPIDelegateBitBash(data_rate, bit_order, mode, cs_pin, this->clk_pin_, this->sdo_pin_, this->sdi_pin_);
}
virtual bool is_hw() { return false; }
protected:
GPIOPin *clk_pin_{};
GPIOPin *sdo_pin_{};
GPIOPin *sdi_pin_{};
};
class SPIClient;
class SPIComponent : public Component {
public:
SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate,
GPIOPin *cs_pin);
void unregister_device(SPIClient *device);
void set_clk(GPIOPin *clk) { this->clk_pin_ = clk; }
void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; }
void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; }
void set_interface(SPIInterface interface) {
this->interface_ = interface;
this->using_hw_ = true;
}
void set_interface_name(const char *name) { this->interface_name_ = name; }
float get_setup_priority() const override { return setup_priority::BUS; }
void setup() override;
void dump_config() override;
protected:
GPIOPin *clk_pin_{nullptr};
GPIOPin *sdi_pin_{nullptr};
GPIOPin *sdo_pin_{nullptr};
SPIInterface interface_{};
bool using_hw_{false};
const char *interface_name_{nullptr};
SPIBus *spi_bus_{};
std::map<SPIClient *, SPIDelegate *> devices_;
static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi);
};
/**
* Base class for SPIDevice, un-templated.
*/
class SPIClient {
public:
SPIClient(SPIBitOrder bit_order, SPIMode mode, uint32_t data_rate)
: bit_order_(bit_order), mode_(mode), data_rate_(data_rate) {}
virtual void spi_setup() {
this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_);
}
virtual void spi_teardown() {
this->parent_->unregister_device(this);
this->delegate_ = SPIDelegate::NULL_DELEGATE;
}
bool spi_is_ready() { return this->delegate_->is_ready(); }
protected:
SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST};
SPIMode mode_{MODE0};
uint32_t data_rate_{1000000};
SPIComponent *parent_{nullptr};
GPIOPin *cs_{nullptr};
SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE};
};
/**
* The SPIDevice is what components using the SPI will create.
*
* @tparam BIT_ORDER
* @tparam CLOCK_POLARITY
* @tparam CLOCK_PHASE
* @tparam DATA_RATE
*/
template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, SPIDataRate DATA_RATE>
class SPIDevice : public SPIClient {
public:
SPIDevice() : SPIClient(BIT_ORDER, Utility::get_mode(CLOCK_POLARITY, CLOCK_PHASE), DATA_RATE) {}
SPIDevice(SPIComponent *parent, GPIOPin *cs_pin) {
this->set_spi_parent(parent);
this->set_cs_pin(cs_pin);
}
void spi_setup() override { SPIClient::spi_setup(); }
void spi_teardown() override { SPIClient::spi_teardown(); }
void set_spi_parent(SPIComponent *parent) { this->parent_ = parent; }
void set_cs_pin(GPIOPin *cs) { this->cs_ = cs; }
void set_data_rate(uint32_t data_rate) { this->data_rate_ = data_rate; }
void set_bit_order(SPIBitOrder order) {
this->bit_order_ = order;
esph_log_d("spi.h", "bit order set to %d", order);
}
void set_mode(SPIMode mode) { this->mode_ = mode; }
uint8_t read_byte() { return this->delegate_->transfer(0); }
void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); }
void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); }
void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); }
uint8_t transfer_byte(uint8_t data) { return this->delegate_->transfer(data); }
// the driver will byte-swap if required.
void write_byte16(uint16_t data) { this->delegate_->write16(data); }
// avoid use of this if possible. It's inefficient and ugly.
void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); }
void enable() { this->delegate_->begin_transaction(); }
void disable() { this->delegate_->end_transaction(); }
void write_array(const uint8_t *data, size_t length) { this->delegate_->write_array(data, length); }
template<size_t N> void write_array(const std::array<uint8_t, N> &data) { this->write_array(data.data(), N); }
void write_array(const std::vector<uint8_t> &data) { this->write_array(data.data(), data.size()); }
uint8_t transfer_byte(uint8_t data) {
return this->parent_->template transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
}
void transfer_array(uint8_t *data, size_t length) {
this->parent_->template transfer_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
}
template<size_t N> void transfer_array(std::array<uint8_t, N> &data) { this->transfer_array(data.data(), N); }
protected:
SPIComponent *parent_{nullptr};
GPIOPin *cs_{nullptr};
};
} // namespace spi

View file

@ -0,0 +1,89 @@
#include "spi.h"
#include <vector>
namespace esphome {
namespace spi {
#ifdef USE_ARDUINO
static const char *const TAG = "spi-esp-arduino";
class SPIDelegateHw : public SPIDelegate {
public:
SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin)
: SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel) {}
void begin_transaction() override {
#ifdef USE_RP2040
SPISettings const settings(this->data_rate_, static_cast<BitOrder>(this->bit_order_), this->mode_);
#else
SPISettings const settings(this->data_rate_, this->bit_order_, this->mode_);
#endif
this->channel_->beginTransaction(settings);
SPIDelegate::begin_transaction();
}
void transfer(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); }
void end_transaction() override {
this->channel_->endTransaction();
SPIDelegate::end_transaction();
}
uint8_t transfer(uint8_t data) override { return this->channel_->transfer(data); }
void write16(uint16_t data) override { this->channel_->transfer16(data); }
#ifdef USE_RP2040
void write_array(const uint8_t *ptr, size_t length) override {
// avoid overwriting the supplied buffer
uint8_t *rxbuf = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory)
memcpy(rxbuf, ptr, length);
this->channel_->transfer((void *) rxbuf, length);
delete[] rxbuf; // NOLINT(cppcoreguidelines-owning-memory)
}
#else
void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); }
#endif
void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); }
protected:
SPIInterface channel_{};
};
class SPIBusHw : public SPIBus {
public:
SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) {
#ifdef USE_ESP8266
channel->pins(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1);
channel->begin();
#endif // USE_ESP8266
#ifdef USE_ESP32
channel->begin(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1);
#endif
#ifdef USE_RP2040
if (Utility::get_pin_no(sdi) != -1)
channel->setRX(Utility::get_pin_no(sdi));
if (Utility::get_pin_no(sdo) != -1)
channel->setTX(Utility::get_pin_no(sdo));
channel->setSCK(Utility::get_pin_no(clk));
channel->begin();
#endif
}
SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override {
return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin);
}
protected:
SPIInterface channel_{};
bool is_hw() override { return true; }
};
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) {
return new SPIBusHw(clk, sdo, sdi, interface);
}
#endif // USE_ARDUINO
} // namespace spi
} // namespace esphome

View file

@ -0,0 +1,163 @@
#include "spi.h"
#include <vector>
namespace esphome {
namespace spi {
#ifdef USE_ESP_IDF
static const char *const TAG = "spi-esp-idf";
static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API.
class SPIDelegateHw : public SPIDelegate {
public:
SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin,
bool write_only)
: SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel), write_only_(write_only) {
spi_device_interface_config_t config = {};
config.mode = static_cast<uint8_t>(mode);
config.clock_speed_hz = static_cast<int>(data_rate);
config.spics_io_num = -1;
config.flags = 0;
config.queue_size = 1;
config.pre_cb = nullptr;
config.post_cb = nullptr;
if (bit_order == BIT_ORDER_LSB_FIRST)
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
if (write_only)
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
esp_err_t const err = spi_bus_add_device(channel, &config, &this->handle_);
if (err != ESP_OK)
ESP_LOGE(TAG, "Add device failed - err %X", err);
}
bool is_ready() override { return this->handle_ != nullptr; }
void begin_transaction() override {
if (this->is_ready()) {
if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK)
ESP_LOGE(TAG, "Failed to acquire SPI bus");
SPIDelegate::begin_transaction();
} else {
ESP_LOGW(TAG, "spi_setup called before initialisation");
}
}
void end_transaction() override {
if (this->is_ready()) {
SPIDelegate::end_transaction();
spi_device_release_bus(this->handle_);
}
}
~SPIDelegateHw() override {
esp_err_t const err = spi_bus_remove_device(this->handle_);
if (err != ESP_OK)
ESP_LOGE(TAG, "Remove device failed - err %X", err);
}
// do a transfer. either txbuf or rxbuf (but not both) may be null.
// transfers above the maximum size will be split.
// TODO - make use of the queue for interrupt transfers to provide a (short) pipeline of blocks
// when splitting is required.
void transfer(const uint8_t *txbuf, uint8_t *rxbuf, size_t length) override {
if (rxbuf != nullptr && this->write_only_) {
ESP_LOGE(TAG, "Attempted read from write-only channel");
return;
}
spi_transaction_t desc = {};
desc.flags = 0;
while (length != 0) {
size_t const partial = std::min(length, MAX_TRANSFER_SIZE);
desc.length = partial * 8;
desc.rxlength = this->write_only_ ? 0 : partial * 8;
desc.tx_buffer = txbuf;
desc.rx_buffer = rxbuf;
esp_err_t const err = spi_device_transmit(this->handle_, &desc);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Transmit failed - err %X", err);
break;
}
length -= partial;
if (txbuf != nullptr)
txbuf += partial;
if (rxbuf != nullptr)
rxbuf += partial;
}
}
void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); }
uint8_t transfer(uint8_t data) override {
uint8_t rxbuf;
this->transfer(&data, &rxbuf, 1);
return rxbuf;
}
void write16(uint16_t data) override {
if (this->bit_order_ == BIT_ORDER_MSB_FIRST) {
uint16_t txbuf = SPI_SWAP_DATA_TX(data, 16);
this->transfer((uint8_t *) &txbuf, nullptr, 2);
} else {
this->transfer((uint8_t *) &data, nullptr, 2);
}
}
void write_array(const uint8_t *ptr, size_t length) override { this->transfer(ptr, nullptr, length); }
void write_array16(const uint16_t *data, size_t length) override {
if (this->bit_order_ == BIT_ORDER_LSB_FIRST) {
this->write_array((uint8_t *) data, length * 2);
} else {
uint16_t buffer[MAX_TRANSFER_SIZE / 2];
while (length != 0) {
size_t const partial = std::min(length, MAX_TRANSFER_SIZE / 2);
for (size_t i = 0; i != partial; i++) {
buffer[i] = SPI_SWAP_DATA_TX(*data++, 16);
}
this->write_array((const uint8_t *) buffer, partial * 2);
length -= partial;
}
}
}
void read_array(uint8_t *ptr, size_t length) override { this->transfer(nullptr, ptr, length); }
protected:
SPIInterface channel_{};
spi_device_handle_t handle_{};
bool write_only_{false};
};
class SPIBusHw : public SPIBus {
public:
SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = Utility::get_pin_no(sdo);
buscfg.miso_io_num = Utility::get_pin_no(sdi);
buscfg.sclk_io_num = Utility::get_pin_no(clk);
buscfg.quadwp_io_num = -1;
buscfg.quadhd_io_num = -1;
buscfg.max_transfer_sz = MAX_TRANSFER_SIZE;
auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO);
if (err != ESP_OK)
ESP_LOGE(TAG, "Bus init failed - err %X", err);
}
SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override {
return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin,
Utility::get_pin_no(this->sdi_pin_) == -1);
}
protected:
SPIInterface channel_{};
bool is_hw() override { return true; }
};
SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) {
return new SPIBusHw(clk, sdo, sdi, interface);
}
#endif
} // namespace spi
} // namespace esphome

View file

@ -0,0 +1,49 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi
from esphome.const import CONF_ID, CONF_DATA_RATE, CONF_MODE
DEPENDENCIES = ["spi"]
CODEOWNERS = ["@clydebarrow"]
MULTI_CONF = True
spi_device_ns = cg.esphome_ns.namespace("spi_device")
spi_device = spi_device_ns.class_("SPIDeviceComponent", cg.Component, spi.SPIDevice)
Mode = spi.spi_ns.enum("SPIMode")
MODES = {
"0": Mode.MODE0,
"1": Mode.MODE1,
"2": Mode.MODE2,
"3": Mode.MODE3,
"MODE0": Mode.MODE0,
"MODE1": Mode.MODE1,
"MODE2": Mode.MODE2,
"MODE3": Mode.MODE3,
}
BitOrder = spi.spi_ns.enum("SPIBitOrder")
ORDERS = {
"msb_first": BitOrder.BIT_ORDER_MSB_FIRST,
"lsb_first": BitOrder.BIT_ORDER_LSB_FIRST,
}
CONF_BIT_ORDER = "bit_order"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(spi_device),
cv.Optional(CONF_DATA_RATE, default="1MHz"): spi.SPI_DATA_RATE_SCHEMA,
cv.Optional(CONF_BIT_ORDER, default="msb_first"): cv.enum(ORDERS, lower=True),
cv.Optional(CONF_MODE, default="0"): cv.enum(MODES, upper=True),
}
).extend(spi.spi_device_schema(False))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_data_rate(config[CONF_DATA_RATE]))
cg.add(var.set_mode(config[CONF_MODE]))
cg.add(var.set_bit_order(config[CONF_BIT_ORDER]))
await spi.register_spi_device(var, config)

View file

@ -0,0 +1,30 @@
#include "spi_device.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace spi_device {
static const char *const TAG = "spi_device";
void SPIDeviceComponent::setup() {
ESP_LOGD(TAG, "Setting up SPIDevice...");
this->spi_setup();
ESP_LOGCONFIG(TAG, "SPIDevice started!");
}
void SPIDeviceComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SPIDevice");
LOG_PIN(" CS pin: ", this->cs_);
ESP_LOGCONFIG(TAG, " Mode: %d", this->mode_);
if (this->data_rate_ < 1000000) {
ESP_LOGCONFIG(TAG, " Data rate: %dkHz", this->data_rate_ / 1000);
} else {
ESP_LOGCONFIG(TAG, " Data rate: %dMHz", this->data_rate_ / 1000000);
}
}
float SPIDeviceComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace spi_device
} // namespace esphome

View file

@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace spi_device {
class SPIDeviceComponent : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
};
} // namespace spi_device
} // namespace esphome

View file

@ -0,0 +1,2 @@
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["spi"]

View file

@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light
from esphome.components import spi
from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_DATA_RATE
spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip")
SpiLedStrip = spi_led_strip_ns.class_(
"SpiLedStrip", light.AddressableLight, spi.SPIDevice
)
CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpiLedStrip),
cv.Optional(CONF_NUM_LEDS, default=1): cv.positive_not_null_int,
cv.Optional(CONF_DATA_RATE, default="1MHz"): spi.SPI_DATA_RATE_SCHEMA,
}
).extend(spi.spi_device_schema(False))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
cg.add(var.set_data_rate(spi.SPI_DATA_RATE_OPTIONS[config[CONF_DATA_RATE]]))
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
await light.register_light(var, config)
await spi.register_spi_device(var, config)
await cg.register_component(var, config)

View file

@ -0,0 +1,91 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace spi_led_strip {
static const char *const TAG = "spi_led_strip";
class SpiLedStrip : public light::AddressableLight,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> {
public:
void setup() { this->spi_setup(); }
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
}
void set_num_leds(uint16_t num_leds) {
this->num_leds_ = num_leds;
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buffer_size_ = num_leds * 4 + 8;
this->buf_ = allocator.allocate(this->buffer_size_);
if (this->buf_ == nullptr) {
esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_);
this->mark_failed();
return;
}
this->effect_data_ = allocator.allocate(num_leds);
if (this->effect_data_ == nullptr) {
esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds);
this->mark_failed();
return;
}
memset(this->buf_, 0xFF, this->buffer_size_);
memset(this->buf_, 0, 4);
}
void dump_config() {
esph_log_config(TAG, "SPI LED Strip:");
esph_log_config(TAG, " LEDs: %d", this->num_leds_);
if (this->data_rate_ >= spi::DATA_RATE_1MHZ)
esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
else
esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000));
}
void write_state(light::LightState *state) override {
if (this->is_failed())
return;
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
char strbuf[49];
size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3);
memset(strbuf, 0, sizeof(strbuf));
for (size_t i = 0; i != len; i++) {
sprintf(strbuf + i * 3, "%02X ", this->buf_[i]);
}
esph_log_v(TAG, "write_state: buf = %s", strbuf);
}
this->enable();
this->write_array(this->buf_, this->buffer_size_);
this->disable();
}
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
size_t pos = index * 4 + 5;
return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr,
this->effect_data_ + index, &this->correction_};
}
size_t buffer_size_{};
uint8_t *effect_data_{nullptr};
uint8_t *buf_{nullptr};
uint16_t num_leds_;
};
} // namespace spi_led_strip
} // namespace esphome

View file

@ -40,6 +40,9 @@ void WiFiComponent::setup() {
if (this->enable_on_boot_) {
this->start();
} else {
#ifdef USE_ESP32
esp_netif_init();
#endif
this->state_ = WIFI_COMPONENT_STATE_DISABLED;
}
}

View file

@ -0,0 +1,113 @@
import re
import ipaddress
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_TIME_ID,
CONF_ADDRESS,
CONF_REBOOT_TIMEOUT,
)
from esphome.components import time
CONF_NETMASK = "netmask"
CONF_PRIVATE_KEY = "private_key"
CONF_PEER_ENDPOINT = "peer_endpoint"
CONF_PEER_PUBLIC_KEY = "peer_public_key"
CONF_PEER_PORT = "peer_port"
CONF_PEER_PRESHARED_KEY = "peer_preshared_key"
CONF_PEER_ALLOWED_IPS = "peer_allowed_ips"
CONF_PEER_PERSISTENT_KEEPALIVE = "peer_persistent_keepalive"
CONF_REQUIRE_CONNECTION_TO_PROCEED = "require_connection_to_proceed"
DEPENDENCIES = ["time", "esp32"]
CODEOWNERS = ["@lhoracek", "@droscy", "@thomas0bernard"]
# The key validation regex has been described by Jason Donenfeld himself
# url: https://lists.zx2c4.com/pipermail/wireguard/2020-December/006222.html
_WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$")
wireguard_ns = cg.esphome_ns.namespace("wireguard")
Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent)
def _wireguard_key(value):
if _WG_KEY_REGEX.match(cv.string(value)) is not None:
return value
raise cv.Invalid(f"Invalid WireGuard key: {value}")
def _cidr_network(value):
try:
ipaddress.ip_network(value, strict=False)
except ValueError as err:
raise cv.Invalid(f"Invalid network in CIDR notation: {err}")
return value
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(Wireguard),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Required(CONF_ADDRESS): cv.ipv4,
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4,
cv.Required(CONF_PRIVATE_KEY): _wireguard_key,
cv.Required(CONF_PEER_ENDPOINT): cv.string,
cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key,
cv.Optional(CONF_PEER_PORT, default=51820): cv.port,
cv.Optional(CONF_PEER_PRESHARED_KEY): _wireguard_key,
cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list(
_cidr_network
),
cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default=0): cv.Any(
cv.positive_time_period_seconds,
cv.uint16_t,
),
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_REQUIRE_CONNECTION_TO_PROCEED, default=False): cv.boolean,
}
).extend(cv.polling_component_schema("10s"))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_address(str(config[CONF_ADDRESS])))
cg.add(var.set_netmask(str(config[CONF_NETMASK])))
cg.add(var.set_private_key(config[CONF_PRIVATE_KEY]))
cg.add(var.set_peer_endpoint(config[CONF_PEER_ENDPOINT]))
cg.add(var.set_peer_public_key(config[CONF_PEER_PUBLIC_KEY]))
cg.add(var.set_peer_port(config[CONF_PEER_PORT]))
cg.add(var.set_keepalive(config[CONF_PEER_PERSISTENT_KEEPALIVE]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
if CONF_PEER_PRESHARED_KEY in config:
cg.add(var.set_preshared_key(config[CONF_PEER_PRESHARED_KEY]))
allowed_ips = list(
ipaddress.collapse_addresses(
[
ipaddress.ip_network(ip, strict=False)
for ip in config[CONF_PEER_ALLOWED_IPS]
]
)
)
for ip in allowed_ips:
cg.add(var.add_allowed_ip(str(ip.network_address), str(ip.netmask)))
cg.add(var.set_srctime(await cg.get_variable(config[CONF_TIME_ID])))
if config[CONF_REQUIRE_CONNECTION_TO_PROCEED]:
cg.add(var.disable_auto_proceed())
# This flag is added here because the esp_wireguard library statically
# set the size of its allowed_ips list at compile time using this value;
# the '+1' modifier is relative to the device's own address that will
# be automatically added to the provided list.
cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}")
cg.add_library("droscy/esp_wireguard", "0.3.2")
await cg.register_component(var, config)

View file

@ -0,0 +1,28 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
CONF_STATUS,
DEVICE_CLASS_CONNECTIVITY,
)
from . import Wireguard
CONF_WIREGUARD_ID = "wireguard_id"
DEPENDENCIES = ["wireguard"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard),
cv.Optional(CONF_STATUS): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
),
}
async def to_code(config):
parent = await cg.get_variable(config[CONF_WIREGUARD_ID])
if status_config := config.get(CONF_STATUS):
sens = await binary_sensor.new_binary_sensor(status_config)
cg.add(parent.set_status_sensor(sens))

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
DEVICE_CLASS_TIMESTAMP,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from . import Wireguard
CONF_WIREGUARD_ID = "wireguard_id"
CONF_LATEST_HANDSHAKE = "latest_handshake"
DEPENDENCIES = ["wireguard"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard),
cv.Optional(CONF_LATEST_HANDSHAKE): sensor.sensor_schema(
device_class=DEVICE_CLASS_TIMESTAMP,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config):
parent = await cg.get_variable(config[CONF_WIREGUARD_ID])
if latest_handshake_config := config.get(CONF_LATEST_HANDSHAKE):
sens = await sensor.new_sensor(latest_handshake_config)
cg.add(parent.set_handshake_sensor(sens))

View file

@ -0,0 +1,296 @@
#include "wireguard.h"
#ifdef USE_ESP32
#include <ctime>
#include <functional>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/time.h"
#include "esphome/components/network/util.h"
#include <esp_err.h>
#include <esp_wireguard.h>
// includes for resume/suspend wdt
#if defined(USE_ESP_IDF)
#include <esp_task_wdt.h>
#if ESP_IDF_VERSION_MAJOR >= 5
#include <spi_flash_mmap.h>
#endif
#elif defined(USE_ARDUINO)
#include <esp32-hal.h>
#endif
namespace esphome {
namespace wireguard {
static const char *const TAG = "wireguard";
static const char *const LOGMSG_PEER_STATUS = "WireGuard remote peer is %s (latest handshake %s)";
static const char *const LOGMSG_ONLINE = "online";
static const char *const LOGMSG_OFFLINE = "offline";
void Wireguard::setup() {
ESP_LOGD(TAG, "initializing WireGuard...");
this->wg_config_.address = this->address_.c_str();
this->wg_config_.private_key = this->private_key_.c_str();
this->wg_config_.endpoint = this->peer_endpoint_.c_str();
this->wg_config_.public_key = this->peer_public_key_.c_str();
this->wg_config_.port = this->peer_port_;
this->wg_config_.netmask = this->netmask_.c_str();
this->wg_config_.persistent_keepalive = this->keepalive_;
if (this->preshared_key_.length() > 0)
this->wg_config_.preshared_key = this->preshared_key_.c_str();
this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_));
if (this->wg_initialized_ == ESP_OK) {
ESP_LOGI(TAG, "WireGuard initialized");
this->wg_peer_offline_time_ = millis();
this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this));
this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup
} else {
ESP_LOGE(TAG, "cannot initialize WireGuard, error code %d", this->wg_initialized_);
this->mark_failed();
}
}
void Wireguard::loop() {
if ((this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && (!network::is_connected())) {
ESP_LOGV(TAG, "local network connection has been lost, stopping WireGuard...");
this->stop_connection_();
}
}
void Wireguard::update() {
bool peer_up = this->is_peer_up();
time_t lhs = this->get_latest_handshake();
bool lhs_updated = (lhs > this->latest_saved_handshake_);
ESP_LOGV(TAG, "handshake: latest=%.0f, saved=%.0f, updated=%d", (double) lhs, (double) this->latest_saved_handshake_,
(int) lhs_updated);
if (lhs_updated) {
this->latest_saved_handshake_ = lhs;
}
std::string latest_handshake =
(this->latest_saved_handshake_ > 0)
? ESPTime::from_epoch_local(this->latest_saved_handshake_).strftime("%Y-%m-%d %H:%M:%S %Z")
: "timestamp not available";
if (peer_up) {
if (this->wg_peer_offline_time_ != 0) {
ESP_LOGI(TAG, LOGMSG_PEER_STATUS, LOGMSG_ONLINE, latest_handshake.c_str());
this->wg_peer_offline_time_ = 0;
} else {
ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_ONLINE, latest_handshake.c_str());
}
} else {
if (this->wg_peer_offline_time_ == 0) {
ESP_LOGW(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str());
this->wg_peer_offline_time_ = millis();
} else {
ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str());
this->start_connection_();
}
// check reboot timeout every time the peer is down
if (this->reboot_timeout_ > 0) {
if (millis() - this->wg_peer_offline_time_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "WireGuard remote peer is unreachable, rebooting...");
App.reboot();
}
}
}
#ifdef USE_BINARY_SENSOR
if (this->status_sensor_ != nullptr) {
this->status_sensor_->publish_state(peer_up);
}
#endif
#ifdef USE_SENSOR
if (this->handshake_sensor_ != nullptr && lhs_updated) {
this->handshake_sensor_->publish_state((double) this->latest_saved_handshake_);
}
#endif
}
void Wireguard::dump_config() {
ESP_LOGCONFIG(TAG, "WireGuard:");
ESP_LOGCONFIG(TAG, " Address: %s", this->address_.c_str());
ESP_LOGCONFIG(TAG, " Netmask: %s", this->netmask_.c_str());
ESP_LOGCONFIG(TAG, " Private Key: " LOG_SECRET("%s"), mask_key(this->private_key_).c_str());
ESP_LOGCONFIG(TAG, " Peer Endpoint: " LOG_SECRET("%s"), this->peer_endpoint_.c_str());
ESP_LOGCONFIG(TAG, " Peer Port: " LOG_SECRET("%d"), this->peer_port_);
ESP_LOGCONFIG(TAG, " Peer Public Key: " LOG_SECRET("%s"), this->peer_public_key_.c_str());
ESP_LOGCONFIG(TAG, " Peer Pre-shared Key: " LOG_SECRET("%s"),
(this->preshared_key_.length() > 0 ? mask_key(this->preshared_key_).c_str() : "NOT IN USE"));
ESP_LOGCONFIG(TAG, " Peer Allowed IPs:");
for (auto &allowed_ip : this->allowed_ips_) {
ESP_LOGCONFIG(TAG, " - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str());
}
ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_,
(this->keepalive_ > 0 ? "s" : " (DISABLED)"));
ESP_LOGCONFIG(TAG, " Reboot Timeout: %d%s", (this->reboot_timeout_ / 1000),
(this->reboot_timeout_ != 0 ? "s" : " (DISABLED)"));
// be careful: if proceed_allowed_ is true, require connection is false
ESP_LOGCONFIG(TAG, " Require Connection to Proceed: %s", (this->proceed_allowed_ ? "NO" : "YES"));
LOG_UPDATE_INTERVAL(this);
}
void Wireguard::on_shutdown() { this->stop_connection_(); }
bool Wireguard::can_proceed() { return (this->proceed_allowed_ || this->is_peer_up()); }
bool Wireguard::is_peer_up() const {
return (this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) &&
(esp_wireguardif_peer_is_up(&(this->wg_ctx_)) == ESP_OK);
}
time_t Wireguard::get_latest_handshake() const {
time_t result;
if (esp_wireguard_latest_handshake(&(this->wg_ctx_), &result) != ESP_OK) {
result = 0;
}
return result;
}
void Wireguard::set_address(const std::string &address) { this->address_ = address; }
void Wireguard::set_netmask(const std::string &netmask) { this->netmask_ = netmask; }
void Wireguard::set_private_key(const std::string &key) { this->private_key_ = key; }
void Wireguard::set_peer_endpoint(const std::string &endpoint) { this->peer_endpoint_ = endpoint; }
void Wireguard::set_peer_public_key(const std::string &key) { this->peer_public_key_ = key; }
void Wireguard::set_peer_port(const uint16_t port) { this->peer_port_ = port; }
void Wireguard::set_preshared_key(const std::string &key) { this->preshared_key_ = key; }
void Wireguard::add_allowed_ip(const std::string &ip, const std::string &netmask) {
this->allowed_ips_.emplace_back(ip, netmask);
}
void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; }
void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; }
void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; }
#ifdef USE_BINARY_SENSOR
void Wireguard::set_status_sensor(binary_sensor::BinarySensor *sensor) { this->status_sensor_ = sensor; }
#endif
#ifdef USE_SENSOR
void Wireguard::set_handshake_sensor(sensor::Sensor *sensor) { this->handshake_sensor_ = sensor; }
#endif
void Wireguard::disable_auto_proceed() { this->proceed_allowed_ = false; }
void Wireguard::start_connection_() {
if (this->wg_initialized_ != ESP_OK) {
ESP_LOGE(TAG, "cannot start WireGuard, initialization in error with code %d", this->wg_initialized_);
return;
}
if (!network::is_connected()) {
ESP_LOGD(TAG, "WireGuard is waiting for local network connection to be available");
return;
}
if (!this->srctime_->now().is_valid()) {
ESP_LOGD(TAG, "WireGuard is waiting for system time to be synchronized");
return;
}
if (this->wg_connected_ == ESP_OK) {
ESP_LOGV(TAG, "WireGuard connection already started");
return;
}
ESP_LOGD(TAG, "starting WireGuard connection...");
/*
* The function esp_wireguard_connect() contains a DNS resolution
* that could trigger the watchdog, so before it we suspend (or
* increase the time, it depends on the platform) the wdt and
* then we resume the normal timeout.
*/
suspend_wdt();
ESP_LOGV(TAG, "executing esp_wireguard_connect");
this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_));
resume_wdt();
if (this->wg_connected_ == ESP_OK) {
ESP_LOGI(TAG, "WireGuard connection started");
} else {
ESP_LOGW(TAG, "cannot start WireGuard connection, error code %d", this->wg_connected_);
return;
}
ESP_LOGD(TAG, "configuring WireGuard allowed IPs list...");
bool allowed_ips_ok = true;
for (std::tuple<std::string, std::string> ip : this->allowed_ips_) {
allowed_ips_ok &=
(esp_wireguard_add_allowed_ip(&(this->wg_ctx_), std::get<0>(ip).c_str(), std::get<1>(ip).c_str()) == ESP_OK);
}
if (allowed_ips_ok) {
ESP_LOGD(TAG, "allowed IPs list configured correctly");
} else {
ESP_LOGE(TAG, "cannot configure WireGuard allowed IPs list, aborting...");
this->stop_connection_();
this->mark_failed();
}
}
void Wireguard::stop_connection_() {
if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) {
ESP_LOGD(TAG, "stopping WireGuard connection...");
esp_wireguard_disconnect(&(this->wg_ctx_));
this->wg_connected_ = ESP_FAIL;
}
}
void suspend_wdt() {
#if defined(USE_ESP_IDF)
#if ESP_IDF_VERSION_MAJOR >= 5
ESP_LOGV(TAG, "temporarily increasing wdt timeout to 15000 ms");
esp_task_wdt_config_t wdtc;
wdtc.timeout_ms = 15000;
wdtc.idle_core_mask = 0;
wdtc.trigger_panic = false;
esp_task_wdt_reconfigure(&wdtc);
#else
ESP_LOGV(TAG, "temporarily increasing wdt timeout to 15 seconds");
esp_task_wdt_init(15, false);
#endif
#elif defined(USE_ARDUINO)
ESP_LOGV(TAG, "temporarily disabling the wdt");
disableLoopWDT();
#endif
}
void resume_wdt() {
#if defined(USE_ESP_IDF)
#if ESP_IDF_VERSION_MAJOR >= 5
wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000;
esp_task_wdt_reconfigure(&wdtc);
ESP_LOGV(TAG, "wdt resumed with %d ms timeout", wdtc.timeout_ms);
#else
esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false);
ESP_LOGV(TAG, "wdt resumed with %d seconds timeout", CONFIG_ESP_TASK_WDT_TIMEOUT_S);
#endif
#elif defined(USE_ARDUINO)
enableLoopWDT();
ESP_LOGV(TAG, "wdt resumed");
#endif
}
std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...]="); }
} // namespace wireguard
} // namespace esphome
#endif // USE_ESP32

View file

@ -0,0 +1,122 @@
#pragma once
#ifdef USE_ESP32
#include <ctime>
#include <vector>
#include <tuple>
#include "esphome/core/component.h"
#include "esphome/components/time/real_time_clock.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#include <esp_wireguard.h>
namespace esphome {
namespace wireguard {
class Wireguard : public PollingComponent {
public:
void setup() override;
void loop() override;
void update() override;
void dump_config() override;
void on_shutdown() override;
bool can_proceed() override;
float get_setup_priority() const override { return esphome::setup_priority::BEFORE_CONNECTION; }
void set_address(const std::string &address);
void set_netmask(const std::string &netmask);
void set_private_key(const std::string &key);
void set_peer_endpoint(const std::string &endpoint);
void set_peer_public_key(const std::string &key);
void set_peer_port(uint16_t port);
void set_preshared_key(const std::string &key);
void add_allowed_ip(const std::string &ip, const std::string &netmask);
void set_keepalive(uint16_t seconds);
void set_reboot_timeout(uint32_t seconds);
void set_srctime(time::RealTimeClock *srctime);
#ifdef USE_BINARY_SENSOR
void set_status_sensor(binary_sensor::BinarySensor *sensor);
#endif
#ifdef USE_SENSOR
void set_handshake_sensor(sensor::Sensor *sensor);
#endif
/// Block the setup step until peer is connected.
void disable_auto_proceed();
bool is_peer_up() const;
time_t get_latest_handshake() const;
protected:
std::string address_;
std::string netmask_;
std::string private_key_;
std::string peer_endpoint_;
std::string peer_public_key_;
std::string preshared_key_;
std::vector<std::tuple<std::string, std::string>> allowed_ips_;
uint16_t peer_port_;
uint16_t keepalive_;
uint32_t reboot_timeout_;
time::RealTimeClock *srctime_;
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *status_sensor_ = nullptr;
#endif
#ifdef USE_SENSOR
sensor::Sensor *handshake_sensor_ = nullptr;
#endif
/// Set to false to block the setup step until peer is connected.
bool proceed_allowed_ = true;
wireguard_config_t wg_config_ = ESP_WIREGUARD_CONFIG_DEFAULT();
wireguard_ctx_t wg_ctx_ = ESP_WIREGUARD_CONTEXT_DEFAULT();
esp_err_t wg_initialized_ = ESP_FAIL;
esp_err_t wg_connected_ = ESP_FAIL;
/// The last time the remote peer become offline.
uint32_t wg_peer_offline_time_ = 0;
/** \brief The latest saved handshake.
*
* This is used to save (and log) the latest completed handshake even
* after a full refresh of the wireguard keys (for example after a
* stop/start connection cycle).
*/
time_t latest_saved_handshake_ = 0;
void start_connection_();
void stop_connection_();
};
// These are used for possibly long DNS resolution to temporarily suspend the watchdog
void suspend_wdt();
void resume_wdt();
/// Strip most part of the key only for secure printing
std::string mask_key(const std::string &key);
} // namespace wireguard
} // namespace esphome
#endif // USE_ESP32

View file

@ -298,6 +298,9 @@ CONF_GLYPHS = "glyphs"
CONF_GPIO = "gpio"
CONF_GREEN = "green"
CONF_GROUP = "group"
CONF_GYROSCOPE_X = "gyroscope_x"
CONF_GYROSCOPE_Y = "gyroscope_y"
CONF_GYROSCOPE_Z = "gyroscope_z"
CONF_HARDWARE_UART = "hardware_uart"
CONF_HEAD = "head"
CONF_HEARTBEAT = "heartbeat"
@ -538,8 +541,11 @@ CONF_PAYLOAD_AVAILABLE = "payload_available"
CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
CONF_PERIOD = "period"
CONF_PH = "ph"
CONF_PHASE_A = "phase_a"
CONF_PHASE_ANGLE = "phase_angle"
CONF_PHASE_B = "phase_b"
CONF_PHASE_BALANCER = "phase_balancer"
CONF_PHASE_C = "phase_c"
CONF_PIN = "pin"
CONF_PIN_A = "pin_a"
CONF_PIN_B = "pin_b"
@ -864,6 +870,9 @@ ICON_FLOWER = "mdi:flower"
ICON_GAS_CYLINDER = "mdi:gas-cylinder"
ICON_GAUGE = "mdi:gauge"
ICON_GRAIN = "mdi:grain"
ICON_GYROSCOPE_X = "mdi:axis-x-rotate-clockwise"
ICON_GYROSCOPE_Y = "mdi:axis-y-rotate-clockwise"
ICON_GYROSCOPE_Z = "mdi:axis-z-rotate-clockwise"
ICON_HEATING_COIL = "mdi:heating-coil"
ICON_KEY_PLUS = "mdi:key-plus"
ICON_LIGHTBULB = "mdi:lightbulb"

View file

@ -554,6 +554,12 @@ class EsphomeCore:
def config_dir(self):
return os.path.dirname(self.config_path)
@property
def data_dir(self):
if is_ha_addon():
return os.path.join("/data")
return self.relative_config_path(".esphome")
@property
def config_filename(self):
return os.path.basename(self.config_path)
@ -563,7 +569,7 @@ class EsphomeCore:
return os.path.join(self.config_dir, path_)
def relative_internal_path(self, *path: str) -> str:
return self.relative_config_path(".esphome", *path)
return os.path.join(self.data_dir, *path)
def relative_build_path(self, *path):
path_ = os.path.expanduser(os.path.join(*path))
@ -573,13 +579,9 @@ class EsphomeCore:
return self.relative_build_path("src", *path)
def relative_pioenvs_path(self, *path):
if is_ha_addon():
return os.path.join("/data", self.name, ".pioenvs", *path)
return self.relative_build_path(".pioenvs", *path)
def relative_piolibdeps_path(self, *path):
if is_ha_addon():
return os.path.join("/data", self.name, ".piolibdeps", *path)
return self.relative_build_path(".piolibdeps", *path)
@property

View file

@ -249,7 +249,11 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
void play_complex(Ts... x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
if (this->count_.value(x...) > 0) {
this->then_.play(0, x...);
} else {
this->play_next_tuple_(this->var_);
}
}
void play(Ts... x) override { /* ignore - see play_complex */

View file

@ -198,8 +198,8 @@ def preload_core_config(config, result):
CORE.data[KEY_CORE] = {}
if CONF_BUILD_PATH not in conf:
conf[CONF_BUILD_PATH] = f".esphome/build/{CORE.name}"
CORE.build_path = CORE.relative_config_path(conf[CONF_BUILD_PATH])
conf[CONF_BUILD_PATH] = f"build/{CORE.name}"
CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH])
has_oldstyle = CONF_PLATFORM in conf
newstyle_found = [key for key in TARGET_PLATFORMS if key in config]

View file

@ -49,6 +49,11 @@ std::string ESPTime::strftime(const std::string &format) {
struct tm c_tm = this->to_c_tm();
size_t len = ::strftime(&timestr[0], timestr.size(), format.c_str(), &c_tm);
while (len == 0) {
if (timestr.size() >= 128) {
// strftime has failed for reasons unrelated to the size of the buffer
// so return a formatting error
return "ERROR";
}
timestr.resize(timestr.size() * 2);
len = ::strftime(&timestr[0], timestr.size(), format.c_str(), &c_tm);
}

View file

@ -45,6 +45,10 @@ struct ESPTime {
*
* @warning This method uses dynamically allocated strings which can cause heap fragmentation with some
* microcontrollers.
*
* @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the
* format string contains unsupported specifiers or when the format string doesn't produce any
* output.
*/
std::string strftime(const std::string &format);

View file

@ -663,7 +663,11 @@ async def process_lambda(
:param return_type: The return type of the lambda.
:return: The generated lambda expression.
"""
from esphome.components.globals import GlobalsComponent, RestoringGlobalsComponent
from esphome.components.globals import (
GlobalsComponent,
RestoringGlobalsComponent,
RestoringGlobalStringComponent,
)
if value is None:
return
@ -676,6 +680,7 @@ async def process_lambda(
and (
full_id.type.inherits_from(GlobalsComponent)
or full_id.type.inherits_from(RestoringGlobalsComponent)
or full_id.type.inherits_from(RestoringGlobalStringComponent)
)
):
parts[i * 3 + 1] = var.value()

View file

@ -32,6 +32,7 @@ import yaml
from tornado.log import access_log
from esphome import const, platformio_api, util, yaml_util
from esphome.core import CORE
from esphome.helpers import get_bool_env, mkdir_p, run_system_command
from esphome.storage_json import (
EsphomeStorageJSON,
@ -70,6 +71,7 @@ class DashboardSettings:
self.password_hash = password_hash(password)
self.config_dir = args.configuration
self.absolute_config_dir = Path(self.config_dir).resolve()
CORE.config_path = os.path.join(self.config_dir, ".")
@property
def relative_url(self):
@ -534,13 +536,16 @@ class DownloadListRequestHandler(BaseHandler):
@authenticated
@bind_config
def get(self, configuration=None):
storage_path = ext_storage_path(settings.config_dir, configuration)
storage_path = ext_storage_path(configuration)
storage_json = StorageJSON.load(storage_path)
if storage_json is None:
self.send_error(404)
return
from esphome.components.esp32 import get_download_types as esp32_types
from esphome.components.esp32 import (
get_download_types as esp32_types,
VARIANTS as ESP32_VARIANTS,
)
from esphome.components.esp8266 import get_download_types as esp8266_types
from esphome.components.rp2040 import get_download_types as rp2040_types
from esphome.components.libretiny import get_download_types as libretiny_types
@ -551,7 +556,7 @@ class DownloadListRequestHandler(BaseHandler):
downloads = rp2040_types(storage_json)
elif platform == const.PLATFORM_ESP8266:
downloads = esp8266_types(storage_json)
elif platform == const.PLATFORM_ESP32:
elif platform.upper() in ESP32_VARIANTS:
downloads = esp32_types(storage_json)
elif platform == const.PLATFORM_BK72XX:
downloads = libretiny_types(storage_json)
@ -574,7 +579,7 @@ class DownloadBinaryRequestHandler(BaseHandler):
def get(self, configuration=None):
compressed = self.get_argument("compressed", "0") == "1"
storage_path = ext_storage_path(settings.config_dir, configuration)
storage_path = ext_storage_path(configuration)
storage_json = StorageJSON.load(storage_path)
if storage_json is None:
self.send_error(404)
@ -663,9 +668,7 @@ class DashboardEntry:
@property
def storage(self) -> Optional[StorageJSON]:
if not self._loaded_storage:
self._storage = StorageJSON.load(
ext_storage_path(settings.config_dir, self.filename)
)
self._storage = StorageJSON.load(ext_storage_path(self.filename))
self._loaded_storage = True
return self._storage
@ -1041,9 +1044,9 @@ class DeleteRequestHandler(BaseHandler):
@bind_config
def post(self, configuration=None):
config_file = settings.rel_path(configuration)
storage_path = ext_storage_path(settings.config_dir, configuration)
storage_path = ext_storage_path(configuration)
trash_path = trash_storage_path(settings.config_dir)
trash_path = trash_storage_path()
mkdir_p(trash_path)
shutil.move(config_file, os.path.join(trash_path, configuration))
@ -1064,7 +1067,7 @@ class UndoDeleteRequestHandler(BaseHandler):
@bind_config
def post(self, configuration=None):
config_file = settings.rel_path(configuration)
trash_path = trash_storage_path(settings.config_dir)
trash_path = trash_storage_path()
shutil.move(os.path.join(trash_path, configuration), config_file)
@ -1322,10 +1325,9 @@ def make_app(debug=get_bool_env(ENV_DEV)):
def start_web_server(args):
settings.parse_args(args)
mkdir_p(settings.rel_path(".esphome"))
if settings.using_auth:
path = esphome_storage_path(settings.config_dir)
path = esphome_storage_path()
storage = EsphomeStorageJSON.load(path)
if storage is None:
storage = EsphomeStorageJSON.get_default()

View file

@ -35,7 +35,7 @@ def run_git_command(cmd, cwd=None) -> str:
def _compute_destination_path(key: str, domain: str) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / domain
base_dir = Path(CORE.data_dir) / domain
h = hashlib.new("sha256")
h.update(key.encode())
return base_dir / h.hexdigest()[:8]

View file

@ -22,19 +22,19 @@ _LOGGER = logging.getLogger(__name__)
def storage_path() -> str:
return CORE.relative_internal_path(f"{CORE.config_filename}.json")
return os.path.join(CORE.data_dir, "storage", f"{CORE.config_filename}.json")
def ext_storage_path(base_path: str, config_filename: str) -> str:
return os.path.join(base_path, ".esphome", f"{config_filename}.json")
def ext_storage_path(config_filename: str) -> str:
return os.path.join(CORE.data_dir, "storage", f"{config_filename}.json")
def esphome_storage_path(base_path: str) -> str:
return os.path.join(base_path, ".esphome", "esphome.json")
def esphome_storage_path() -> str:
return os.path.join(CORE.data_dir, "esphome.json")
def trash_storage_path(base_path: str) -> str:
return os.path.join(base_path, ".esphome", "trash")
def trash_storage_path() -> str:
return CORE.relative_config_path("trash")
class StorageJSON:

View file

@ -6,12 +6,12 @@ import unicodedata
import voluptuous as vol
import esphome.config_validation as cv
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
from esphome.core import CORE
from esphome.helpers import get_bool_env, write_file
from esphome.log import color, Fore
from esphome.log import Fore, color
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.util import safe_print
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
CORE_BIG = r""" _____ ____ _____ ______
/ ____/ __ \| __ \| ____|
@ -193,10 +193,10 @@ captive_portal:
def wizard_write(path, **kwargs):
from esphome.components.esp8266 import boards as esp8266_boards
from esphome.components.esp32 import boards as esp32_boards
from esphome.components.rp2040 import boards as rp2040_boards
from esphome.components.bk72xx import boards as bk72xx_boards
from esphome.components.esp32 import boards as esp32_boards
from esphome.components.esp8266 import boards as esp8266_boards
from esphome.components.rp2040 import boards as rp2040_boards
from esphome.components.rtl87xx import boards as rtl87xx_boards
name = kwargs["name"]
@ -225,7 +225,7 @@ def wizard_write(path, **kwargs):
write_file(path, wizard_file(**kwargs))
storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware)
storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path))
storage_path = ext_storage_path(os.path.basename(path))
storage.save(storage_path)
return True
@ -265,9 +265,9 @@ def strip_accents(value):
def wizard(path):
from esphome.components.bk72xx import boards as bk72xx_boards
from esphome.components.esp32 import boards as esp32_boards
from esphome.components.esp8266 import boards as esp8266_boards
from esphome.components.bk72xx import boards as bk72xx_boards
from esphome.components.rtl87xx import boards as rtl87xx_boards
if not path.endswith(".yaml") and not path.endswith(".yml"):
@ -280,6 +280,9 @@ def wizard(path):
f"Uh oh, it seems like {color(Fore.CYAN, path)} already exists, please delete that file first or chose another configuration file."
)
return 2
CORE.config_path = path
safe_print("Hi there!")
sleep(1.5)
safe_print("I'm the wizard of ESPHome :)")

View file

@ -123,6 +123,7 @@ lib_deps =
DNSServer ; captive_portal (Arduino built-in)
esphome/ESP32-audioI2S@2.0.7 ; i2s_audio
crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir
droscy/esp_wireguard@0.3.2 ; wireguard
build_flags =
${common:arduino.build_flags}
-DUSE_ESP32
@ -141,6 +142,7 @@ framework = espidf
lib_deps =
${common:idf.lib_deps}
espressif/esp32-camera@1.0.0 ; esp32_camera
droscy/esp_wireguard@0.3.2 ; wireguard
build_flags =
${common:idf.build_flags}
-Wno-nonnull-compare

View file

@ -11,7 +11,7 @@ esptool==4.6.2
click==8.1.7
esphome-dashboard==20230904.0
aioesphomeapi==15.0.0
zeroconf==0.88.0
zeroconf==0.102.0
# esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View file

@ -5,7 +5,7 @@ pyupgrade==3.10.1 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==7.4.1
pytest==7.4.2
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-asyncio==0.21.1

View file

@ -27,3 +27,4 @@ Current test_.yaml file contents.
| test6.yaml | RP2040 | wifi | N/A
| test7.yaml | ESP32-C3 | wifi | N/A
| test8.yaml | ESP32-S3 | wifi | None
| test10.yaml | ESP32 | wifi | None

View file

@ -915,6 +915,23 @@ sensor:
temperature:
name: MPU6886 Temperature
i2c_id: i2c_bus
- platform: bmi160
address: 0x68
acceleration_x:
name: BMI160 Accel X
acceleration_y:
name: BMI160 Accel Y
acceleration_z:
name: BMI160 Accel z
gyroscope_x:
name: BMI160 Gyro X
gyroscope_y:
name: BMI160 Gyro Y
gyroscope_z:
name: BMI160 Gyro z
temperature:
name: BMI160 Temperature
i2c_id: i2c_bus
- platform: mmc5603
address: 0x30
field_strength_x:

48
tests/test10.yaml Normal file
View file

@ -0,0 +1,48 @@
---
esphome:
name: test10
build_path: build/test10
esp32:
board: esp32doit-devkit-v1
framework:
type: arduino
wifi:
ssid: "MySSID1"
password: "password1"
reboot_timeout: 3min
power_save_mode: high
logger:
level: VERBOSE
api:
reboot_timeout: 10min
time:
- platform: sntp
wireguard:
id: vpn
address: 172.16.34.100
netmask: 255.255.255.0
# NEVER use the following keys for your vpn, they are now public!
private_key: wPBMxtNYH3mChicrbpsRpZIasIdPq3yZuthn23FbGG8=
peer_public_key: Hs2JfikvYU03/Kv3YoAs1hrUIPPTEkpsZKSPUljE9yc=
peer_preshared_key: 20fjM5GRnSolGPC5SRj9ljgIUyQfruv0B0bvLl3Yt60=
peer_endpoint: wg.server.example
peer_persistent_keepalive: 25s
peer_allowed_ips:
- 172.16.34.0/24
- 192.168.4.0/24
binary_sensor:
- platform: wireguard
status:
name: 'WireGuard Status'
sensor:
- platform: wireguard
latest_handshake:
name: 'WireGuard Latest Handshake'

View file

@ -5,6 +5,13 @@ esphome:
board: nodemcu-32s
build_path: build/test2
globals:
- id: my_global_string
type: std::string
restore_value: yes
max_restore_data_length: 70
initial_value: '"DefaultValue"'
substitutions:
devicename: test2

View file

@ -32,6 +32,7 @@ spi:
clk_pin: GPIO21
mosi_pin: GPIO22
miso_pin: GPIO23
interface: hardware
uart:
- id: uart115200

View file

@ -563,6 +563,13 @@ script:
then:
- logger.log: looping!
- id: zero_repeat_test
then:
- repeat:
count: !lambda "return 0;"
then:
- logger.log: shouldn't see mee!
switch:
- platform: modbus_controller
modbus_controller_id: modbus_controller_test

View file

@ -62,3 +62,6 @@ switch:
sensor:
- platform: internal_temperature
name: Internal Temperature
- platform: adc
pin: VCC
name: VSYS

View file

@ -29,10 +29,25 @@ light:
name: neopixel-enable
internal: false
restore_mode: ALWAYS_OFF
- platform: spi_led_strip
num_leds: 4
color_correct: [80%, 60%, 100%]
id: rgb_led
name: "RGB LED"
data_rate: 8MHz
spi:
id: spi_id_1
clk_pin: GPIO7
mosi_pin: GPIO6
interface: any
spi_device:
id: spidev
data_rate: 2MHz
spi_id: spi_id_1
mode: 3
bit_order: lsb_first
display:
- platform: ili9xxx

View file

@ -1,7 +1,9 @@
"""Tests for the wizard.py file."""
import os
import esphome.wizard as wz
import pytest
from esphome.core import CORE
from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
from esphome.components.esp32.boards import ESP32_BOARD_PINS
from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
@ -110,6 +112,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch):
# Given
del default_config["platform"]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)
@ -130,6 +133,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266(
default_config["board"] = [*ESP8266_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)
@ -150,6 +154,7 @@ def test_wizard_write_defaults_platform_from_board_esp32(
default_config["board"] = [*ESP32_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)
@ -170,6 +175,7 @@ def test_wizard_write_defaults_platform_from_board_bk72xx(
default_config["board"] = [*BK72XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)
@ -190,6 +196,7 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx(
default_config["board"] = [*RTL87XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)