Merge branch 'dev' into optolink

This commit is contained in:
j0ta29 2023-12-17 18:07:43 +01:00 committed by GitHub
commit e89c810ed9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
291 changed files with 12404 additions and 2270 deletions

View file

@ -37,6 +37,7 @@
"!secret scalar",
"!lambda scalar",
"!extend scalar",
"!remove scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",

View file

@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment

View file

@ -42,7 +42,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: "3.9"
- name: Set up Docker Buildx

View file

@ -40,7 +40,7 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment

View file

@ -18,7 +18,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.1
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""

View file

@ -45,7 +45,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: "3.x"
- name: Set up python environment
@ -80,7 +80,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: "3.9"

View file

@ -18,7 +18,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8.0.0
- uses: actions/stale@v9.0.0
with:
days-before-pr-stale: 90
days-before-pr-close: 7
@ -38,7 +38,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8.0.0
- uses: actions/stale@v9.0.0
with:
days-before-pr-stale: -1
days-before-pr-close: -1

View file

@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: 3.11

View file

@ -19,4 +19,4 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Run yamllint
uses: frenck/action-yamllint@v1.4.1
uses: frenck/action-yamllint@v1.4.2

View file

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

View file

@ -12,6 +12,7 @@ esphome/core/* @esphome/core
# Integrations
esphome/components/a01nyub/* @MrSuicideParrot
esphome/components/a02yyuw/* @TH-Braemer
esphome/components/absolute_humidity/* @DAVe3283
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
@ -88,7 +89,7 @@ esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/* @jesserockz
esphome/components/ektf2232/touchscreen/* @jesserockz
esphome/components/emc2101/* @ellull
esphome/components/ens160/* @vincentscode
esphome/components/ens210/* @itn3rd77
@ -110,19 +111,24 @@ esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier
esphome/components/globals/* @esphome/core
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode
@ -235,11 +241,17 @@ esphome/components/pmwcs3/* @SeByDocKy
esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
esphome/components/pn532_spi/* @OttoWinter @jesserockz
esphome/components/pn7150/* @jesserockz @kbx81
esphome/components/pn7150_i2c/* @jesserockz @kbx81
esphome/components/pn7160/* @jesserockz @kbx81
esphome/components/pn7160_i2c/* @jesserockz @kbx81
esphome/components/pn7160_spi/* @jesserockz @kbx81
esphome/components/power_supply/* @esphome/core
esphome/components/preferences/* @esphome/core
esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter
esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qwiic_pir/* @kahrendt
@ -328,7 +340,7 @@ esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath
esphome/components/tof10120/* @wstrzalka
esphome/components/toshiba/* @kbx81
esphome/components/touchscreen/* @jesserockz
esphome/components/touchscreen/* @jesserockz @nielsnl68
esphome/components/tsl2591/* @wjcarpenter
esphome/components/tt21100/* @kroimon
esphome/components/tuya/binary_sensor/* @jesserockz
@ -361,6 +373,6 @@ esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/* @nielsnl68 @numo68
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt

View file

@ -10,5 +10,3 @@ Things to note when contributing:
for more information.
- Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files
which checks if your new feature compiles correctly.
- Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping
me after some time.

View file

@ -389,7 +389,8 @@ def command_config(args, config):
output = re.sub(
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
)
safe_print(output)
if not CORE.quiet:
safe_print(output)
_LOGGER.info("Configuration is valid!")
return 0

View file

@ -8,50 +8,37 @@ namespace esphome {
namespace a01nyub {
static const char *const TAG = "a01nyub.sensor";
static const uint8_t MAX_DATA_LENGTH_BYTES = 4;
void A01nyubComponent::loop() {
uint8_t data;
while (this->available() > 0) {
if (this->read_byte(&data)) {
buffer_.push_back(data);
this->read_byte(&data);
if (this->buffer_.empty() && (data != 0xff))
continue;
buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
}
}
void A01nyubComponent::check_buffer_() {
if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) {
size_t i;
for (i = 0; i < this->buffer_.size(); i++) {
// Look for the first packet
if (this->buffer_[i] == 0xFF) {
if (i + 1 + 3 < this->buffer_.size()) { // Packet is not complete
return; // Wait for completion
}
uint8_t checksum = (this->buffer_[i] + this->buffer_[i + 1] + this->buffer_[i + 2]) & 0xFF;
if (this->buffer_[i + 3] == checksum) {
float distance = (this->buffer_[i + 1] << 8) + this->buffer_[i + 2];
if (distance > 280) {
float meters = distance / 1000.0;
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
}
break;
}
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
if (this->buffer_[3] == checksum) {
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
if (distance > 280) {
float meters = distance / 1000.0;
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
this->buffer_.clear();
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
}
this->buffer_.clear();
}
void A01nyubComponent::dump_config() {
ESP_LOGCONFIG(TAG, "A01nyub Sensor:");
LOG_SENSOR(" ", "Distance", this);
}
void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); }
} // namespace a01nyub
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@TH-Braemer"]

View file

@ -0,0 +1,43 @@
// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311
#include "a02yyuw.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace a02yyuw {
static const char *const TAG = "a02yyuw.sensor";
void A02yyuwComponent::loop() {
uint8_t data;
while (this->available() > 0) {
this->read_byte(&data);
if (this->buffer_.empty() && (data != 0xff))
continue;
buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
}
void A02yyuwComponent::check_buffer_() {
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
if (this->buffer_[3] == checksum) {
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
if (distance > 30) {
ESP_LOGV(TAG, "Distance from sensor: %f mm", distance);
this->publish_state(distance);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
}
this->buffer_.clear();
}
void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); }
} // namespace a02yyuw
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace a02yyuw {
class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
// Nothing really public.
// ========== INTERNAL METHODS ==========
void loop() override;
void dump_config() override;
protected:
void check_buffer_();
std::vector<uint8_t> buffer_;
};
} // namespace a02yyuw
} // namespace esphome

View file

@ -0,0 +1,41 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.const import (
STATE_CLASS_MEASUREMENT,
ICON_ARROW_EXPAND_VERTICAL,
DEVICE_CLASS_DISTANCE,
)
CODEOWNERS = ["@TH-Braemer"]
DEPENDENCIES = ["uart"]
UNIT_MILLIMETERS = "mm"
a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw")
A02yyuwComponent = a02yyuw_ns.class_(
"A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice
)
CONFIG_SCHEMA = sensor.sensor_schema(
A02yyuwComponent,
unit_of_measurement=UNIT_MILLIMETERS,
icon=ICON_ARROW_EXPAND_VERTICAL,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_DISTANCE,
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"a02yyuw",
baud_rate=9600,
require_tx=False,
require_rx=True,
data_bits=8,
parity=None,
stop_bits=1,
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ANALOG, CONF_INPUT
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER
from esphome.core import CORE
from esphome.components.esp32 import get_esp32_variant
@ -152,7 +152,8 @@ def validate_adc_pin(value):
return cv.only_on_rp2040("TEMPERATURE")
if CORE.is_esp32:
value = pins.internal_gpio_input_pin_number(value)
conf = pins.internal_gpio_input_pin_schema(value)
value = conf[CONF_NUMBER]
variant = get_esp32_variant()
if (
variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
@ -166,24 +167,23 @@ def validate_adc_pin(value):
):
raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
return pins.internal_gpio_input_pin_schema(value)
return conf
if CORE.is_esp8266:
value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})(
value
)
if value != 17: # A0
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
return pins.gpio_pin_schema(
conf = pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
if conf[CONF_NUMBER] != 17: # A0
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
return conf
if CORE.is_rp2040:
value = pins.internal_gpio_input_pin_number(value)
if value not in (26, 27, 28, 29):
conf = pins.internal_gpio_input_pin_schema(value)
number = conf[CONF_NUMBER]
if number not in (26, 27, 28, 29):
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
return pins.internal_gpio_input_pin_schema(value)
return conf
if CORE.is_libretiny:
return pins.gpio_pin_schema(

View file

@ -10,7 +10,7 @@
namespace esphome {
namespace addressable_light {
class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent {
class AddressableLightDisplay : public display::DisplayBuffer {
public:
light::AddressableLight *get_light() const { return this->light_; }

View file

@ -45,7 +45,6 @@ async def to_code(config):
cg.add(var.set_height(config[CONF_HEIGHT]))
cg.add(var.set_light(wrapped_light))
await cg.register_component(var, config)
await display.register_display(var, config)
if pixel_mapper := config.get(CONF_PIXEL_MAPPER):

View file

@ -21,36 +21,49 @@ namespace esphome {
namespace aht10 {
static const char *const TAG = "aht10";
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1};
static const size_t SIZE_CALIBRATE_CMD = 3;
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00};
static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00};
static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement
static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms
static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms
static const uint8_t AHT10_CAL_ATTEMPTS = 10;
static const uint8_t AHT10_STATUS_BUSY = 0x80;
void AHT10Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up AHT10...");
const uint8_t *calibrate_cmd;
switch (this->variant_) {
case AHT10Variant::AHT20:
calibrate_cmd = AHT20_CALIBRATE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT20");
break;
case AHT10Variant::AHT10:
default:
calibrate_cmd = AHT10_CALIBRATE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT10");
}
if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) {
if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
uint8_t data = 0;
if (this->write(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
uint8_t data = AHT10_STATUS_BUSY;
int cal_attempts = 0;
while (data & AHT10_STATUS_BUSY) {
delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
++cal_attempts;
if (cal_attempts > AHT10_CAL_ATTEMPTS) {
ESP_LOGE(TAG, "AHT10 calibration timed out!");
this->mark_failed();
return;
}
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
ESP_LOGE(TAG, "AHT10 calibration failed!");
@ -62,7 +75,7 @@ void AHT10Component::setup() {
}
void AHT10Component::update() {
if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) {
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
return;
@ -89,7 +102,7 @@ void AHT10Component::update() {
break;
} else {
ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) {
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
return;

View file

@ -1,5 +1,7 @@
#pragma once
#include <utility>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
@ -7,12 +9,15 @@
namespace esphome {
namespace aht10 {
enum AHT10Variant { AHT10, AHT20 };
class AHT10Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override;
void set_variant(AHT10Variant variant) { this->variant_ = variant; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@ -20,6 +25,7 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
AHT10Variant variant_{};
};
} // namespace aht10

View file

@ -10,6 +10,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
CONF_VARIANT,
)
DEPENDENCIES = ["i2c"]
@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"]
aht10_ns = cg.esphome_ns.namespace("aht10")
AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice)
AHT10Variant = aht10_ns.enum("AHT10Variant")
AHT10_VARIANTS = {
"AHT10": AHT10Variant.AHT10,
"AHT20": AHT10Variant.AHT20,
}
CONFIG_SCHEMA = (
cv.Schema(
{
@ -33,6 +40,9 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum(
AHT10_VARIANTS, upper=True
),
}
)
.extend(cv.polling_component_schema("60s"))
@ -44,6 +54,7 @@ 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)
cg.add(var.set_variant(config[CONF_VARIANT]))
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)

View file

@ -365,6 +365,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9;
string icon = 10;
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
}
enum FanSpeed {
FAN_SPEED_LOW = 0;
@ -387,6 +388,7 @@ message FanStateResponse {
FanSpeed speed = 4 [deprecated = true];
FanDirection direction = 5;
int32 speed_level = 6;
string preset_mode = 7;
}
message FanCommandRequest {
option (id) = 31;
@ -405,6 +407,8 @@ message FanCommandRequest {
FanDirection direction = 9;
bool has_speed_level = 10;
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
}
// ==================== LIGHT ====================
@ -855,6 +859,10 @@ message ListEntitiesClimateResponse {
string icon = 19;
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
}
message ClimateStateResponse {
option (id) = 47;
@ -875,6 +883,8 @@ message ClimateStateResponse {
string custom_fan_mode = 11;
ClimatePreset preset = 12;
string custom_preset = 13;
float current_humidity = 14;
float target_humidity = 15;
}
message ClimateCommandRequest {
option (id) = 48;
@ -903,6 +913,8 @@ message ClimateCommandRequest {
ClimatePreset preset = 19;
bool has_custom_preset = 20;
string custom_preset = 21;
bool has_target_humidity = 22;
float target_humidity = 23;
}
// ==================== NUMBER ====================

View file

@ -293,6 +293,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
}
if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes())
resp.preset_mode = fan->preset_mode;
return this->send_fan_state_response(resp);
}
bool APIConnection::send_fan_info(fan::Fan *fan) {
@ -307,6 +309,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) {
msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count();
for (auto const &preset : traits.supported_preset_modes())
msg.supported_preset_modes.push_back(preset);
msg.disabled_by_default = fan->is_disabled_by_default();
msg.icon = fan->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category());
@ -328,6 +332,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
}
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.perform();
}
#endif
@ -554,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
resp.custom_preset = climate->custom_preset.value();
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
resp.target_humidity = climate->target_humidity;
return this->send_climate_state_response(resp);
}
bool APIConnection::send_climate_info(climate::Climate *climate) {
@ -570,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category());
msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
for (auto mode : traits.get_supported_modes())
msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
@ -579,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
msg.supports_action = traits.get_supports_action();
@ -609,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_target_temperature_high)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_target_humidity)
call.set_target_humidity(msg.target_humidity);
if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode)

View file

@ -1375,6 +1375,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi
this->icon = value.as_string();
return true;
}
case 12: {
this->supported_preset_modes.push_back(value.as_string());
return true;
}
default:
return false;
}
@ -1401,6 +1405,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(9, this->disabled_by_default);
buffer.encode_string(10, this->icon);
buffer.encode_enum<enums::EntityCategory>(11, this->entity_category);
for (auto &it : this->supported_preset_modes) {
buffer.encode_string(12, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesFanResponse::dump_to(std::string &out) const {
@ -1451,6 +1458,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
for (const auto &it : this->supported_preset_modes) {
out.append(" supported_preset_modes: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
#endif
@ -1480,6 +1493,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
return false;
}
}
bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 7: {
this->preset_mode = value.as_string();
return true;
}
default:
return false;
}
}
bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
@ -1497,6 +1520,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::FanSpeed>(4, this->speed);
buffer.encode_enum<enums::FanDirection>(5, this->direction);
buffer.encode_int32(6, this->speed_level);
buffer.encode_string(7, this->preset_mode);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void FanStateResponse::dump_to(std::string &out) const {
@ -1527,6 +1551,10 @@ void FanStateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%" PRId32, this->speed_level);
out.append(buffer);
out.append("\n");
out.append(" preset_mode: ");
out.append("'").append(this->preset_mode).append("'");
out.append("\n");
out.append("}");
}
#endif
@ -1572,6 +1600,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
this->speed_level = value.as_int32();
return true;
}
case 12: {
this->has_preset_mode = value.as_bool();
return true;
}
default:
return false;
}
}
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 13: {
this->preset_mode = value.as_string();
return true;
}
default:
return false;
}
@ -1598,6 +1640,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::FanDirection>(9, this->direction);
buffer.encode_bool(10, this->has_speed_level);
buffer.encode_int32(11, this->speed_level);
buffer.encode_bool(12, this->has_preset_mode);
buffer.encode_string(13, this->preset_mode);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void FanCommandRequest::dump_to(std::string &out) const {
@ -1648,6 +1692,14 @@ void FanCommandRequest::dump_to(std::string &out) const {
sprintf(buffer, "%" PRId32, this->speed_level);
out.append(buffer);
out.append("\n");
out.append(" has_preset_mode: ");
out.append(YESNO(this->has_preset_mode));
out.append("\n");
out.append(" preset_mode: ");
out.append("'").append(this->preset_mode).append("'");
out.append("\n");
out.append("}");
}
#endif
@ -3559,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 22: {
this->supports_current_humidity = value.as_bool();
return true;
}
case 23: {
this->supports_target_humidity = value.as_bool();
return true;
}
default:
return false;
}
@ -3615,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val
this->visual_current_temperature_step = value.as_float();
return true;
}
case 24: {
this->visual_min_humidity = value.as_float();
return true;
}
case 25: {
this->visual_max_humidity = value.as_float();
return true;
}
default:
return false;
}
@ -3653,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(19, this->icon);
buffer.encode_enum<enums::EntityCategory>(20, this->entity_category);
buffer.encode_float(21, this->visual_current_temperature_step);
buffer.encode_bool(22, this->supports_current_humidity);
buffer.encode_bool(23, this->supports_target_humidity);
buffer.encode_float(24, this->visual_min_humidity);
buffer.encode_float(25, this->visual_max_humidity);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesClimateResponse::dump_to(std::string &out) const {
@ -3758,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->visual_current_temperature_step);
out.append(buffer);
out.append("\n");
out.append("}");
out.append(" supports_current_humidity: ");
out.append(YESNO(this->supports_current_humidity));
out.append("\n");
out.append(" supports_target_humidity: ");
out.append(YESNO(this->supports_target_humidity));
out.append("\n");
out.append(" visual_min_humidity: ");
sprintf(buffer, "%g", this->visual_min_humidity);
out.append(buffer);
out.append("\n");
out.append(" visual_max_humidity: ");
sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer);
out.append("\n");
}
#endif
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -3827,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
this->target_temperature_high = value.as_float();
return true;
}
case 14: {
this->current_humidity = value.as_float();
return true;
}
case 15: {
this->target_humidity = value.as_float();
return true;
}
default:
return false;
}
@ -3845,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(11, this->custom_fan_mode);
buffer.encode_enum<enums::ClimatePreset>(12, this->preset);
buffer.encode_string(13, this->custom_preset);
buffer.encode_float(14, this->current_humidity);
buffer.encode_float(15, this->target_humidity);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ClimateStateResponse::dump_to(std::string &out) const {
@ -3906,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const {
out.append(" custom_preset: ");
out.append("'").append(this->custom_preset).append("'");
out.append("\n");
out.append("}");
out.append(" current_humidity: ");
sprintf(buffer, "%g", this->current_humidity);
out.append(buffer);
out.append("\n");
out.append(" target_humidity: ");
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
}
#endif
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -3971,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
this->has_custom_preset = value.as_bool();
return true;
}
case 22: {
this->has_target_humidity = value.as_bool();
return true;
}
default:
return false;
}
@ -4007,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
this->target_temperature_high = value.as_float();
return true;
}
case 23: {
this->target_humidity = value.as_float();
return true;
}
default:
return false;
}
@ -4033,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::ClimatePreset>(19, this->preset);
buffer.encode_bool(20, this->has_custom_preset);
buffer.encode_string(21, this->custom_preset);
buffer.encode_bool(22, this->has_target_humidity);
buffer.encode_float(23, this->target_humidity);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ClimateCommandRequest::dump_to(std::string &out) const {
@ -4125,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
out.append(" custom_preset: ");
out.append("'").append(this->custom_preset).append("'");
out.append("\n");
out.append(" has_target_humidity: ");
out.append(YESNO(this->has_target_humidity));
out.append("\n");
out.append(" target_humidity: ");
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif

View file

@ -472,6 +472,7 @@ class ListEntitiesFanResponse : public ProtoMessage {
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
std::vector<std::string> supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -490,6 +491,7 @@ class FanStateResponse : public ProtoMessage {
enums::FanSpeed speed{};
enums::FanDirection direction{};
int32_t speed_level{0};
std::string preset_mode{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -497,6 +499,7 @@ class FanStateResponse : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class FanCommandRequest : public ProtoMessage {
@ -512,6 +515,8 @@ class FanCommandRequest : public ProtoMessage {
enums::FanDirection direction{};
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -519,6 +524,7 @@ class FanCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesLightResponse : public ProtoMessage {
@ -979,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage {
std::string icon{};
enums::EntityCategory entity_category{};
float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false};
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1004,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage {
std::string custom_fan_mode{};
enums::ClimatePreset preset{};
std::string custom_preset{};
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1037,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage {
enums::ClimatePreset preset{};
bool has_custom_preset{false};
std::string custom_preset{};
bool has_target_humidity{false};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;

View file

@ -15,6 +15,16 @@ void BangBangClimate::setup() {
this->publish_state();
});
this->current_temperature = this->sensor_->state;
// register for humidity values and get initial state
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
}
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) {
climate::ClimateTraits BangBangClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
});
@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }

View file

@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component {
void dump_config() override;
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
void set_supports_cool(bool supports_cool);
@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component {
/// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr};
/// The sensor used for getting the current humidity
sensor::Sensor *humidity_sensor_{nullptr};
/** The trigger to call when the controller should switch to idle mode.
*
* In idle mode, the controller is assumed to have both heating and cooling disabled.

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_HEAT_ACTION,
CONF_HUMIDITY_SENSOR,
CONF_ID,
CONF_IDLE_ACTION,
CONF_SENSOR,
@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
@ -47,6 +49,10 @@ async def to_code(config):
sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens))
if CONF_HUMIDITY_SENSOR in config:
sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR])
cg.add(var.set_humidity_sensor(sens))
normal_config = BangBangClimateTargetTempConfig(
config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],

View file

@ -90,40 +90,41 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
void BP1658CJ::write_bit_(bool value) {
this->data_pin_->digital_write(value);
this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
}
void BP1658CJ::write_byte_(uint8_t data) {
for (uint8_t mask = 0x80; mask; mask >>= 1) {
this->write_bit_(data & mask);
delayMicroseconds(BP1658CJ_DELAY);
}
// ack bit
this->data_pin_->pin_mode(gpio::FLAG_INPUT);
this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
this->data_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) {
this->data_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
this->clock_pin_->digital_write(false);
delayMicroseconds(BP1658CJ_DELAY);
for (uint32_t i = 0; i < size; i++) {
this->write_byte_(buffer[i]);
delayMicroseconds(BP1658CJ_DELAY);
}
this->clock_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
this->data_pin_->digital_write(true);
delayMicroseconds(BP1658CJ_DELAY);
}
} // namespace bp1658cj

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_AWAY,
CONF_AWAY_COMMAND_TOPIC,
CONF_AWAY_STATE_TOPIC,
CONF_CURRENT_HUMIDITY_STATE_TOPIC,
CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
CONF_CUSTOM_FAN_MODE,
CONF_CUSTOM_PRESET,
@ -28,6 +29,8 @@ from esphome.const import (
CONF_SWING_MODE,
CONF_SWING_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_STATE_TOPIC,
CONF_TARGET_HUMIDITY_COMMAND_TOPIC,
CONF_TARGET_HUMIDITY_STATE_TOPIC,
CONF_TARGET_TEMPERATURE,
CONF_TARGET_TEMPERATURE_COMMAND_TOPIC,
CONF_TARGET_TEMPERATURE_STATE_TOPIC,
@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = {
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit(
"visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?"
@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA,
cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
}
),
cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All(
@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_ON_CONTROL): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger),
@ -238,6 +255,10 @@ async def setup_climate_core_(var, config):
visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE],
)
)
if CONF_MIN_HUMIDITY in visual:
cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY]))
if CONF_MAX_HUMIDITY in visual:
cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY]))
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
@ -255,6 +276,12 @@ async def setup_climate_core_(var, config):
config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC]
)
)
if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config:
cg.add(
mqtt_.set_custom_current_humidity_state_topic(
config[CONF_CURRENT_HUMIDITY_STATE_TOPIC]
)
)
if CONF_FAN_MODE_COMMAND_TOPIC in config:
cg.add(
mqtt_.set_custom_fan_mode_command_topic(
@ -323,6 +350,18 @@ async def setup_climate_core_(var, config):
config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC]
)
)
if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config:
cg.add(
mqtt_.set_custom_target_humidity_command_topic(
config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC]
)
)
if CONF_TARGET_HUMIDITY_STATE_TOPIC in config:
cg.add(
mqtt_.set_custom_target_humidity_state_topic(
config[CONF_TARGET_HUMIDITY_STATE_TOPIC]
)
)
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
validate_climate_fan_mode
@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args):
config[CONF_TARGET_TEMPERATURE_HIGH], args, float
)
cg.add(var.set_target_temperature_high(template_))
if CONF_TARGET_HUMIDITY in config:
template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float)
cg.add(var.set_target_humidity(template_))
if CONF_FAN_MODE in config:
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
cg.add(var.set_fan_mode(template_))

View file

@ -14,6 +14,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(float, target_temperature_low)
TEMPLATABLE_VALUE(float, target_temperature_high)
TEMPLATABLE_VALUE(float, target_humidity)
TEMPLATABLE_VALUE(bool, away)
TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
TEMPLATABLE_VALUE(std::string, custom_fan_mode)
@ -27,6 +28,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
call.set_target_temperature(this->target_temperature_.optional_value(x...));
call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...));
call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...));
call.set_target_humidity(this->target_humidity_.optional_value(x...));
if (away_.has_value()) {
call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME);
}

View file

@ -45,6 +45,9 @@ void ClimateCall::perform() {
if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
}
if (this->target_humidity_.has_value()) {
ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_);
}
this->parent_->control(*this);
}
void ClimateCall::validate_() {
@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h
this->target_temperature_high_ = target_temperature_high;
return *this;
}
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperat
this->target_temperature_ = target_temperature;
return *this;
}
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
this->mode_ = mode;
return *this;
@ -343,6 +356,9 @@ void Climate::save_state_() {
} else {
state.target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
state.target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value();
@ -408,6 +424,12 @@ void Climate::publish_state() {
} else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
}
if (traits.get_supports_current_humidity()) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.get_supports_target_humidity()) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
// Send state to frontend
this->state_callback_.call(*this);
@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() {
traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_);
traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_);
}
if (this->visual_min_humidity_override_.has_value()) {
traits.set_visual_min_humidity(*this->visual_min_humidity_override_);
}
if (this->visual_max_humidity_override_.has_value()) {
traits.set_visual_max_humidity(*this->visual_max_humidity_override_);
}
return traits;
}
@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current)
this->visual_target_temperature_step_override_ = target;
this->visual_current_temperature_step_override_ = current;
}
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
this->visual_min_humidity_override_ = visual_min_humidity_override;
}
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
this->visual_max_humidity_override_ = visual_max_humidity_override;
}
ClimateCall Climate::make_call() { return ClimateCall(this); }
@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
} else {
call.set_target_temperature(this->target_temperature);
}
if (traits.get_supports_target_humidity()) {
call.set_target_humidity(this->target_humidity);
}
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
call.set_fan_mode(this->fan_mode);
}
@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
} else {
climate->target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
climate->target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
climate->fan_mode = this->fan_mode;
}
@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) {
auto traits = this->get_traits();
ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag, " [x] Visual settings:");
ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Step:");
ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Temperature step:");
ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step());
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity());
ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity());
if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
}
if (traits.get_supports_current_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_two_point_target_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
}
if (traits.get_supports_target_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_action()) {
ESP_LOGCONFIG(tag, " [x] Supports action");
}

View file

@ -64,6 +64,10 @@ class ClimateCall {
* For climate devices with two point target temperature control
*/
ClimateCall &set_target_temperature_high(optional<float> target_temperature_high);
/// Set the target humidity of the climate device.
ClimateCall &set_target_humidity(float target_humidity);
/// Set the target humidity of the climate device.
ClimateCall &set_target_humidity(optional<float> target_humidity);
/// Set the fan mode of the climate device.
ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
/// Set the fan mode of the climate device.
@ -93,6 +97,7 @@ class ClimateCall {
const optional<float> &get_target_temperature() const;
const optional<float> &get_target_temperature_low() const;
const optional<float> &get_target_temperature_high() const;
const optional<float> &get_target_humidity() const;
const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<std::string> &get_custom_fan_mode() const;
@ -107,6 +112,7 @@ class ClimateCall {
optional<float> target_temperature_;
optional<float> target_temperature_low_;
optional<float> target_temperature_high_;
optional<float> target_humidity_;
optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_;
optional<std::string> custom_fan_mode_;
@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState {
float target_temperature_high;
};
};
float target_humidity;
/// Convert this struct to a climate call that can be performed.
ClimateCall to_call(Climate *climate);
@ -160,24 +167,34 @@ struct ClimateDeviceRestoreState {
*/
class Climate : public EntityBase {
public:
Climate() {}
/// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF};
/// The active state of the climate device.
ClimateAction action{CLIMATE_ACTION_OFF};
/// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN};
/// The current humidity of the climate device, as reported from the integration.
float current_humidity{NAN};
union {
/// The target temperature of the climate device.
float target_temperature;
struct {
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_low;
float target_temperature_low{NAN};
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_high;
float target_temperature_high{NAN};
};
};
/// The target humidity of the climate device.
float target_humidity;
/// The active fan mode of the climate device.
optional<ClimateFanMode> fan_mode;
@ -231,6 +248,8 @@ class Climate : public EntityBase {
void set_visual_min_temperature_override(float visual_min_temperature_override);
void set_visual_max_temperature_override(float visual_max_temperature_override);
void set_visual_temperature_step_override(float target, float current);
void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override);
protected:
friend ClimateCall;
@ -280,6 +299,8 @@ class Climate : public EntityBase {
optional<float> visual_max_temperature_override_{};
optional<float> visual_target_temperature_step_override_{};
optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{};
};
} // namespace climate

View file

@ -44,10 +44,18 @@ class ClimateTraits {
void set_supports_current_temperature(bool supports_current_temperature) {
supports_current_temperature_ = supports_current_temperature;
}
bool get_supports_current_humidity() const { return supports_current_humidity_; }
void set_supports_current_humidity(bool supports_current_humidity) {
supports_current_humidity_ = supports_current_humidity;
}
bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; }
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
supports_two_point_target_temperature_ = supports_two_point_target_temperature;
}
bool get_supports_target_humidity() const { return supports_target_humidity_; }
void set_supports_target_humidity(bool supports_target_humidity) {
supports_target_humidity_ = supports_target_humidity;
}
void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
@ -153,6 +161,11 @@ class ClimateTraits {
int8_t get_target_temperature_accuracy_decimals() const;
int8_t get_current_temperature_accuracy_decimals() const;
float get_visual_min_humidity() const { return visual_min_humidity_; }
void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; }
float get_visual_max_humidity() const { return visual_max_humidity_; }
void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; }
protected:
void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) {
@ -177,7 +190,9 @@ class ClimateTraits {
}
bool supports_current_temperature_{false};
bool supports_current_humidity_{false};
bool supports_two_point_target_temperature_{false};
bool supports_target_humidity_{false};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
bool supports_action_{false};
std::set<climate::ClimateFanMode> supported_fan_modes_;
@ -190,6 +205,8 @@ class ClimateTraits {
float visual_max_temperature_{30};
float visual_target_temperature_step_{0.1};
float visual_current_temperature_step_{0.1};
float visual_min_humidity_{30};
float visual_max_humidity_{99};
};
} // namespace climate

View file

@ -12,6 +12,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating;
this->speed = source_->speed;
this->direction = source_->direction;
this->preset_mode = source_->preset_mode;
this->publish_state();
});
@ -19,6 +20,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating;
this->speed = source_->speed;
this->direction = source_->direction;
this->preset_mode = source_->preset_mode;
this->publish_state();
}
@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() {
traits.set_speed(base.supports_speed());
traits.set_supported_speed_count(base.supported_speed_count());
traits.set_direction(base.supports_direction());
traits.set_supported_preset_modes(base.supported_preset_modes());
return traits;
}
@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) {
call2.set_speed(*call.get_speed());
if (call.get_direction().has_value())
call2.set_direction(*call.get_direction());
if (!call.get_preset_mode().empty())
call2.set_preset_mode(call.get_preset_mode());
call2.perform();
}

View file

@ -58,7 +58,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_LAMBDA): cv.lambda_,
}
)
).extend(cv.polling_component_schema("1s"))
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
@ -116,6 +116,7 @@ async def setup_display_core_(var, config):
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)

View file

@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in
}
#endif // USE_QR_CODE
#ifdef USE_GRAPHICAL_DISPLAY_MENU
void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) {
Rect rect(x, y, width, height);
menu->draw(this, &rect);
}
#endif // USE_GRAPHICAL_DISPLAY_MENU
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
int *width, int *height) {
int x_offset, baseline;

View file

@ -17,6 +17,10 @@
#include "esphome/components/qr_code/qr_code.h"
#endif
#ifdef USE_GRAPHICAL_DISPLAY_MENU
#include "esphome/components/graphical_display_menu/graphical_display_menu.h"
#endif
namespace esphome {
namespace display {
@ -163,7 +167,7 @@ class BaseFont {
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
};
class Display {
class Display : public PollingComponent {
public:
/// Fill the entire screen with the given color.
virtual void fill(Color color);
@ -392,6 +396,17 @@ class Display {
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
#endif
#ifdef USE_GRAPHICAL_DISPLAY_MENU
/**
* @param x The x coordinate of the upper left corner
* @param y The y coordinate of the upper left corner
* @param menu The GraphicalDisplayMenu to draw
* @param width Width of the menu
* @param height Height of the menu
*/
void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height);
#endif // USE_GRAPHICAL_DISPLAY_MENU
/** Get the text bounds of the given string.
*
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.

View file

@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() {
this->process_initial_();
this->on_before_show();
if (this->active_ && this->editing_)
this->finish_editing_();
@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() {
}
this->draw_and_update();
this->on_after_show();
}
void DisplayMenuComponent::show() {
@ -196,18 +200,26 @@ void DisplayMenuComponent::show() {
this->process_initial_();
this->on_before_show();
if (!this->active_) {
this->active_ = true;
this->draw_and_update();
}
this->on_after_show();
}
void DisplayMenuComponent::hide() {
if (this->check_healthy_and_active_()) {
this->on_before_hide();
if (this->editing_)
this->finish_editing_();
this->active_ = false;
this->update();
this->on_after_hide();
}
}

View file

@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component {
update();
}
virtual void on_before_show(){};
virtual void on_after_show(){};
virtual void on_before_hide(){};
virtual void on_after_hide(){};
uint8_t rows_;
bool active_;
MenuMode mode_;

View file

@ -5,6 +5,29 @@
namespace esphome {
namespace display_menu_base {
const LogString *menu_item_type_to_string(MenuItemType type) {
switch (type) {
case MenuItemType::MENU_ITEM_LABEL:
return LOG_STR("MENU_ITEM_LABEL");
case MenuItemType::MENU_ITEM_MENU:
return LOG_STR("MENU_ITEM_MENU");
case MenuItemType::MENU_ITEM_BACK:
return LOG_STR("MENU_ITEM_BACK");
case MenuItemType::MENU_ITEM_SELECT:
return LOG_STR("MENU_ITEM_SELECT");
case MenuItemType::MENU_ITEM_NUMBER:
return LOG_STR("MENU_ITEM_NUMBER");
case MenuItemType::MENU_ITEM_SWITCH:
return LOG_STR("MENU_ITEM_SWITCH");
case MenuItemType::MENU_ITEM_COMMAND:
return LOG_STR("MENU_ITEM_COMMAND");
case MenuItemType::MENU_ITEM_CUSTOM:
return LOG_STR("MENU_ITEM_CUSTOM");
default:
return LOG_STR("UNKNOWN");
}
}
void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }

View file

@ -14,6 +14,7 @@
#endif
#include <vector>
#include "esphome/core/log.h"
namespace esphome {
namespace display_menu_base {
@ -29,6 +30,9 @@ enum MenuItemType {
MENU_ITEM_CUSTOM,
};
/// @brief Returns a string representation of a menu item type suitable for logging
const LogString *menu_item_type_to_string(MenuItemType type);
class MenuItem;
class MenuItemMenu;
using value_getter_t = std::function<std::string(const MenuItem *)>;

View file

@ -12,7 +12,6 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232")
EKTF2232Touchscreen = ektf2232_ns.class_(
"EKTF2232Touchscreen",
touchscreen.Touchscreen,
cg.Component,
i2c.I2CDevice,
)
@ -28,17 +27,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
),
cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x15))
.extend(cv.COMPONENT_SCHEMA)
).extend(i2c.i2c_device_schema(0x15))
)
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)
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
cg.add(var.set_interrupt_pin(interrupt_pin))

View file

@ -15,16 +15,12 @@ static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00};
static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00};
static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01};
void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; }
void EKTF2232Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen...");
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->interrupt_pin_->setup();
this->store_.pin = this->interrupt_pin_->to_isr();
this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_,
gpio::INTERRUPT_FALLING_EDGE);
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
this->rts_pin_->setup();
@ -45,7 +41,7 @@ void EKTF2232Touchscreen::setup() {
this->mark_failed();
return;
}
this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->write(GET_Y_RES, 4);
if (this->read(received, 4)) {
@ -54,19 +50,14 @@ void EKTF2232Touchscreen::setup() {
this->mark_failed();
return;
}
this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->store_.touch = false;
this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
this->set_power_state(true);
}
void EKTF2232Touchscreen::loop() {
if (!this->store_.touch)
return;
this->store_.touch = false;
void EKTF2232Touchscreen::update_touches() {
uint8_t touch_count = 0;
std::vector<TouchPoint> touches;
int16_t x_raw, y_raw;
uint8_t raw[8];
this->read(raw, 8);
@ -75,45 +66,15 @@ void EKTF2232Touchscreen::loop() {
touch_count++;
}
if (touch_count == 0) {
for (auto *listener : this->touch_listeners_)
listener->release();
return;
}
touch_count = std::min<uint8_t>(touch_count, 2);
ESP_LOGV(TAG, "Touch count: %d", touch_count);
for (int i = 0; i < touch_count; i++) {
uint8_t *d = raw + 1 + (i * 3);
uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1];
uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2];
raw_x = raw_x * this->display_height_ - 1;
raw_y = raw_y * this->display_width_ - 1;
TouchPoint tp;
switch (this->rotation_) {
case ROTATE_0_DEGREES:
tp.y = raw_x / this->x_resolution_;
tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_);
break;
case ROTATE_90_DEGREES:
tp.x = raw_x / this->x_resolution_;
tp.y = raw_y / this->y_resolution_;
break;
case ROTATE_180_DEGREES:
tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_);
tp.x = raw_y / this->y_resolution_;
break;
case ROTATE_270_DEGREES:
tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_);
tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_);
break;
}
this->defer([this, tp]() { this->send_touch_(tp); });
x_raw = (d[0] & 0xF0) << 4 | d[1];
y_raw = (d[0] & 0x0F) << 8 | d[2];
this->set_raw_touch_position_(i, x_raw, y_raw);
}
}
@ -126,7 +87,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) {
bool EKTF2232Touchscreen::get_power_state() {
uint8_t received[4];
this->write(GET_POWER_STATE_CMD, 4);
this->store_.touch = false;
this->store_.touched = false;
this->read(received, 4);
return (received[1] >> 3) & 1;
}
@ -145,14 +106,14 @@ bool EKTF2232Touchscreen::soft_reset_() {
uint8_t received[4];
uint16_t timeout = 1000;
while (!this->store_.touch && timeout > 0) {
while (!this->store_.touched && timeout > 0) {
delay(1);
timeout--;
}
if (timeout > 0)
this->store_.touch = true;
this->store_.touched = true;
this->read(received, 4);
this->store_.touch = false;
this->store_.touched = false;
return !memcmp(received, HELLO, 4);
}

View file

@ -9,19 +9,11 @@
namespace esphome {
namespace ektf2232 {
struct EKTF2232TouchscreenStore {
volatile bool touch;
ISRInternalGPIOPin pin;
static void gpio_intr(EKTF2232TouchscreenStore *store);
};
using namespace touchscreen;
class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice {
class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
@ -33,12 +25,10 @@ class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2
protected:
void hard_reset_();
bool soft_reset_();
void update_touches() override;
InternalGPIOPin *interrupt_pin_;
GPIOPin *rts_pin_;
EKTF2232TouchscreenStore store_;
uint16_t x_resolution_;
uint16_t y_resolution_;
};
} // namespace ektf2232

View file

@ -9,7 +9,7 @@ from esphome.const import (
CONF_TVOC,
DEVICE_CLASS_AQI,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
ICON_CHEMICAL_WEAPON,
ICON_MOLECULE_CO2,
ICON_RADIATOR,
@ -45,11 +45,10 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,

View file

@ -462,7 +462,7 @@ async def to_code(config):
add_extra_script(
"post",
"post_build2.py",
"post_build.py",
os.path.join(os.path.dirname(__file__), "post_build.py.script"),
)
@ -497,10 +497,11 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
else:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@ -639,20 +640,22 @@ def _write_sdkconfig():
# Called by writer.py
def copy_files():
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if CORE.using_esp_idf:
_write_sdkconfig()
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,
# and no version.txt file exists, the CMake script fails for some setups.

View file

@ -3,15 +3,13 @@ from typing import Any
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
CONF_IGNORE_STRAPPING_WARNING,
PLATFORM_ESP32,
)
from esphome import pins
from esphome.core import CORE
@ -33,7 +31,6 @@ from .const import (
esp32_ns,
)
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
@ -42,7 +39,6 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
@ -161,33 +157,22 @@ DRIVE_STRENGTHS = {
}
gpio_num_t = cg.global_ns.enum("gpio_num_t")
CONF_DRIVE_STRENGTH = "drive_strength"
ESP32_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean,
cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All(
cv.float_with_unit("current", "mA", optional_unit=True),
cv.enum(DRIVE_STRENGTHS),
),
},
pins.gpio_base_schema(ESP32InternalGPIOPin, validate_gpio_pin).extend(
{
cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean,
cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All(
cv.float_with_unit("current", "mA", optional_unit=True),
cv.enum(DRIVE_STRENGTHS),
),
}
),
validate_supports,
)
@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA)
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
async def esp32_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]

View file

@ -25,6 +25,11 @@ AUTO_LOAD = ["psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
# Triggers
ESP32CameraImageTrigger = esp32_camera_ns.class_(
"ESP32CameraImageTrigger", automation.Trigger.template()
)
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
"ESP32CameraStreamStartTrigger",
automation.Trigger.template(),
@ -139,6 +144,7 @@ CONF_IDLE_FRAMERATE = "idle_framerate"
# stream trigger
CONF_ON_STREAM_START = "on_stream_start"
CONF_ON_STREAM_STOP = "on_stream_stop"
CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2)
@ -221,6 +227,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
),
}
),
cv.Optional(CONF_ON_IMAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -289,3 +300,9 @@ async def to_code(config):
for conf in config.get(CONF_ON_STREAM_STOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_IMAGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(ESP32CameraImageData, "image")], conf
)

View file

@ -335,8 +335,8 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
}
/* ---------------- public API (specific) ---------------- */
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
this->new_image_callback_.add(std::move(f));
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) {
this->new_image_callback_.add(std::move(callback));
}
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
this->stream_start_callback_.add(std::move(callback));

View file

@ -86,6 +86,11 @@ class CameraImage {
uint8_t requesters_;
};
struct CameraImageData {
uint8_t *data;
size_t length;
};
/* ---------------- CameraImageReader class ---------------- */
class CameraImageReader {
public:
@ -147,12 +152,12 @@ class ESP32Camera : public Component, public EntityBase {
void dump_config() override;
float get_setup_priority() const override;
/* public API (specific) */
void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f);
void start_stream(CameraRequester requester);
void stop_stream(CameraRequester requester);
void request_image(CameraRequester requester);
void update_camera_parameters();
void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback);
void add_stream_start_callback(std::function<void()> &&callback);
void add_stream_stop_callback(std::function<void()> &&callback);
@ -196,7 +201,7 @@ class ESP32Camera : public Component, public EntityBase {
uint8_t stream_requesters_{0};
QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_;
CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_;
CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_{};
CallbackManager<void()> stream_start_callback_{};
CallbackManager<void()> stream_stop_callback_{};
@ -207,6 +212,18 @@ class ESP32Camera : public Component, public EntityBase {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32Camera *global_esp32_camera;
class ESP32CameraImageTrigger : public Trigger<CameraImageData> {
public:
explicit ESP32CameraImageTrigger(ESP32Camera *parent) {
parent->add_image_callback([this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
CameraImageData camera_image_data{};
camera_image_data.length = image->get_data_length();
camera_image_data.data = image->get_data_buffer();
this->trigger(camera_image_data);
});
}
};
class ESP32CameraStreamStartTrigger : public Trigger<> {
public:
explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {

View file

@ -12,6 +12,7 @@ from esphome.const import (
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
PLATFORM_ESP8266,
)
from esphome import pins
from esphome.core import CORE, coroutine_with_priority
@ -21,10 +22,8 @@ import esphome.codegen as cg
from . import boards
from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
_LOGGER = logging.getLogger(__name__)
ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin)
@ -124,6 +123,8 @@ def validate_supports(value):
(True, False, False, False, False),
# OUTPUT
(False, True, False, False, False),
# INPUT and OUTPUT, e.g. for i2c
(True, True, False, False, False),
# INPUT_PULLUP
(True, False, False, True, False),
# INPUT_PULLDOWN_16
@ -142,21 +143,11 @@ def validate_supports(value):
ESP8266_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ESP8266GPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_ANALOG, default=False): cv.boolean,
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
},
pins.gpio_base_schema(
ESP8266GPIOPin,
validate_gpio_pin,
modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
),
validate_supports,
)
@ -167,7 +158,7 @@ class PinInitialState:
level: int = 255
@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA)
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA)
async def esp8266_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]

View file

@ -18,6 +18,7 @@ from esphome.const import (
CONF_ON_SPEED_SET,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
CONF_ON_PRESET_SET,
CONF_TRIGGER_ID,
CONF_DIRECTION,
CONF_RESTORE_MODE,
@ -57,6 +58,9 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action)
FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template())
FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template()
)
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
@ -101,9 +105,46 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger),
}
),
cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger),
}
),
}
)
_PRESET_MODES_SCHEMA = cv.All(
cv.ensure_list(cv.string_strict),
cv.Length(min=1),
)
def validate_preset_modes(value):
# Check against defined schema
value = _PRESET_MODES_SCHEMA(value)
# Ensure preset names are unique
errors = []
presets = set()
for i, preset in enumerate(value):
# If name does not exist yet add it
if preset not in presets:
presets.add(preset)
continue
# Otherwise it's an error
errors.append(
cv.Invalid(
f"Found duplicate preset name '{preset}'. Presets must have unique names.",
[i],
)
)
if errors:
raise cv.MultipleInvalid(errors)
return value
async def setup_fan_core_(var, config):
await setup_entity(var, config)
@ -154,6 +195,9 @@ async def setup_fan_core_(var, config):
for conf in config.get(CONF_ON_SPEED_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_fan(var, config):

View file

@ -165,5 +165,23 @@ class FanSpeedSetTrigger : public Trigger<> {
int last_speed_;
};
class FanPresetSetTrigger : public Trigger<> {
public:
FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() {
auto preset_mode = state->preset_mode;
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger();
}
});
this->last_preset_mode_ = state->preset_mode;
}
protected:
std::string last_preset_mode_;
};
} // namespace fan
} // namespace esphome

View file

@ -32,9 +32,12 @@ void FanCall::perform() {
if (this->direction_.has_value()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
}
if (!this->preset_mode_.empty()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str());
}
this->parent_.control(*this);
}
void FanCall::validate_() {
auto traits = this->parent_.get_traits();
@ -62,6 +65,15 @@ void FanCall::validate_() {
ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str());
this->direction_.reset();
}
if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes();
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(),
this->preset_mode_.c_str());
this->preset_mode_.clear();
}
}
}
FanCall FanRestoreState::to_call(Fan &fan) {
@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) {
call.set_oscillating(this->oscillating);
call.set_speed(this->speed);
call.set_direction(this->direction);
if (fan.get_traits().supports_preset_modes()) {
// Use stored preset index to get preset name
const auto &preset_modes = fan.get_traits().supported_preset_modes();
if (this->preset_mode < preset_modes.size()) {
call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode));
}
}
return call;
}
void FanRestoreState::apply(Fan &fan) {
@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) {
fan.oscillating = this->oscillating;
fan.speed = this->speed;
fan.direction = this->direction;
if (fan.get_traits().supports_preset_modes()) {
// Use stored preset index to get preset name
const auto &preset_modes = fan.get_traits().supported_preset_modes();
if (this->preset_mode < preset_modes.size()) {
fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode);
}
}
fan.publish_state();
}
@ -100,7 +128,9 @@ void Fan::publish_state() {
if (traits.supports_direction()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
}
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str());
}
this->state_callback_.call();
this->save_state_();
}
@ -143,20 +173,36 @@ void Fan::save_state_() {
state.oscillating = this->oscillating;
state.speed = this->speed;
state.direction = this->direction;
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
const auto &preset_modes = this->get_traits().supported_preset_modes();
// Store index of current preset mode
auto preset_iterator = preset_modes.find(this->preset_mode);
if (preset_iterator != preset_modes.end())
state.preset_mode = std::distance(preset_modes.begin(), preset_iterator);
}
this->rtc_.save(&state);
}
void Fan::dump_traits_(const char *tag, const char *prefix) {
if (this->get_traits().supports_speed()) {
auto traits = this->get_traits();
if (traits.supports_speed()) {
ESP_LOGCONFIG(tag, "%s Speed: YES", prefix);
ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count());
ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, traits.supported_speed_count());
}
if (this->get_traits().supports_oscillation()) {
if (traits.supports_oscillation()) {
ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix);
}
if (this->get_traits().supports_direction()) {
if (traits.supports_direction()) {
ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
}
if (traits.supports_preset_modes()) {
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
for (const std::string &s : traits.supported_preset_modes())
ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str());
}
}
} // namespace fan

View file

@ -72,6 +72,11 @@ class FanCall {
return *this;
}
optional<FanDirection> get_direction() const { return this->direction_; }
FanCall &set_preset_mode(const std::string &preset_mode) {
this->preset_mode_ = preset_mode;
return *this;
}
std::string get_preset_mode() const { return this->preset_mode_; }
void perform();
@ -83,6 +88,7 @@ class FanCall {
optional<bool> oscillating_;
optional<int> speed_;
optional<FanDirection> direction_{};
std::string preset_mode_{};
};
struct FanRestoreState {
@ -90,6 +96,7 @@ struct FanRestoreState {
int speed;
bool oscillating;
FanDirection direction;
uint8_t preset_mode;
/// Convert this struct to a fan call that can be performed.
FanCall to_call(Fan &fan);
@ -107,6 +114,8 @@ class Fan : public EntityBase {
int speed{0};
/// The current direction of the fan
FanDirection direction{FanDirection::FORWARD};
// The current preset mode of the fan
std::string preset_mode{};
FanCall turn_on();
FanCall turn_off();

View file

@ -1,3 +1,6 @@
#include <set>
#include <utility>
#pragma once
namespace esphome {
@ -25,12 +28,19 @@ class FanTraits {
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan.
std::set<std::string> supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan.
void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
/// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
protected:
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
int speed_count_{};
std::set<std::string> preset_modes_{};
};
} // namespace fan

View file

@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c"]
ft5x06_ns = cg.esphome_ns.namespace("ft5x06")

View file

@ -0,0 +1,26 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, touchscreen
from esphome.const import CONF_ID
from .. import ft5x06_ns
FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener")
FT5x06Touchscreen = ft5x06_ns.class_(
"FT5x06Touchscreen",
touchscreen.Touchscreen,
cg.Component,
i2c.I2CDevice,
)
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(FT5x06Touchscreen),
}
).extend(i2c.i2c_device_schema(0x48))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await i2c.register_i2c_device(var, config)
await touchscreen.register_touchscreen(var, config)

View file

@ -0,0 +1,124 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ft5x06 {
static const char *const TAG = "ft5x06.touchscreen";
enum VendorId {
FT5X06_ID_UNKNOWN = 0,
FT5X06_ID_1 = 0x51,
FT5X06_ID_2 = 0x11,
FT5X06_ID_3 = 0xCD,
};
enum FTCmd : uint8_t {
FT5X06_MODE_REG = 0x00,
FT5X06_ORIGIN_REG = 0x08,
FT5X06_RESOLUTION_REG = 0x0C,
FT5X06_VENDOR_ID_REG = 0xA8,
FT5X06_TD_STATUS = 0x02,
FT5X06_TOUCH_DATA = 0x03,
FT5X06_I_MODE = 0xA4,
FT5X06_TOUCH_MAX = 0x4C,
};
enum FTMode : uint8_t {
FT5X06_OP_MODE = 0,
FT5X06_SYSINFO_MODE = 0x10,
FT5X06_TEST_MODE = 0x40,
};
static const size_t MAX_TOUCHES = 5; // max number of possible touches reported
class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override {
esph_log_config(TAG, "Setting up FT5x06 Touchscreen...");
// wait 200ms after reset.
this->set_timeout(200, [this] { this->continue_setup_(); });
}
void continue_setup_(void) {
uint8_t data[4];
if (!this->set_mode_(FT5X06_OP_MODE))
return;
if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID"))
return;
switch (data[0]) {
case FT5X06_ID_1:
case FT5X06_ID_2:
case FT5X06_ID_3:
this->vendor_id_ = (VendorId) data[0];
esph_log_d(TAG, "Read vendor ID 0x%X", data[0]);
break;
default:
esph_log_e(TAG, "Unknown vendor ID 0x%X", data[0]);
this->mark_failed();
return;
}
// reading the chip registers to get max x/y does not seem to work.
this->x_raw_max_ = this->display_->get_width();
this->y_raw_max_ = this->display_->get_height();
esph_log_config(TAG, "FT5x06 Touchscreen setup complete");
}
void update_touches() override {
uint8_t touch_cnt;
uint8_t data[MAX_TOUCHES][6];
if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) {
esph_log_w(TAG, "Failed to read status");
return;
}
if (touch_cnt == 0)
return;
if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) {
esph_log_w(TAG, "Failed to read touch data");
return;
}
for (uint8_t i = 0; i != touch_cnt; i++) {
uint8_t status = data[i][0] >> 6;
uint8_t id = data[i][2] >> 3;
uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]);
uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]);
esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
if (status == 0 || status == 2) {
this->set_raw_touch_position_(id, x, y);
}
}
}
void dump_config() override {
esph_log_config(TAG, "FT5x06 Touchscreen:");
esph_log_config(TAG, " Address: 0x%02X", this->address_);
esph_log_config(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_);
}
protected:
bool err_check_(i2c::ErrorCode err, const char *msg) {
if (err != i2c::ERROR_OK) {
this->mark_failed();
esph_log_e(TAG, "%s failed - err 0x%X", msg, err);
return false;
}
return true;
}
bool set_mode_(FTMode mode) {
return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode");
}
VendorId vendor_id_{FT5X06_ID_UNKNOWN};
};
} // namespace ft5x06
} // namespace esphome

View file

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

View file

@ -0,0 +1,99 @@
/**************************************************************************/
/*!
Author: Gustavo Ambrozio
Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U)
*/
/**************************************************************************/
#include "ft63x6.h"
#include "esphome/core/log.h"
// Registers
// Reference: https://focuslcds.com/content/FT6236.pdf
namespace esphome {
namespace ft63x6 {
static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02;
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03;
static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09;
static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B;
static const char *const TAG = "FT63X6Touchscreen";
void FT63X6Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen...");
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
}
this->hard_reset_();
// Get touch resolution
this->x_raw_max_ = 320;
this->y_raw_max_ = 480;
}
void FT63X6Touchscreen::update_touches() {
int touch_count = this->read_touch_count_();
if (touch_count == 0) {
return;
}
uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1
int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X);
int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y);
this->set_raw_touch_position_(touch_id, x, y);
if (touch_count >= 2) {
touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01)
x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X);
y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y);
this->set_raw_touch_position_(touch_id, x, y);
}
}
void FT63X6Touchscreen::hard_reset_() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
}
void FT63X6Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); }
// Touch functions
uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) {
uint8_t read_buf[2];
read_buf[0] = this->read_byte_(coordinate);
read_buf[1] = this->read_byte_(coordinate + 1);
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
}
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; }
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) {
uint8_t byte = 0;
this->read_byte(addr, &byte);
return byte;
}
} // namespace ft63x6
} // namespace esphome

View file

@ -0,0 +1,41 @@
/**************************************************************************/
/*!
Author: Gustavo Ambrozio
Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U)
*/
/**************************************************************************/
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
namespace esphome {
namespace ft63x6 {
using namespace touchscreen;
class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void hard_reset_();
uint8_t read_byte_(uint8_t addr);
void update_touches() override;
InternalGPIOPin *interrupt_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
uint8_t read_touch_count_();
uint16_t read_touch_coordinate_(uint8_t coordinate);
uint8_t read_touch_id_(uint8_t id_address);
};
} // namespace ft63x6
} // namespace esphome

View file

@ -0,0 +1,44 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c, touchscreen
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
CODEOWNERS = ["@gpambrozio"]
DEPENDENCIES = ["i2c"]
ft6336u_ns = cg.esphome_ns.namespace("ft63x6")
FT63X6Touchscreen = ft6336u_ns.class_(
"FT63X6Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONF_FT63X6_ID = "ft63x6_id"
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FT63X6Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): cv.All(
pins.internal_gpio_input_pin_schema
),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
).extend(i2c.i2c_device_schema(0x38))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN):
interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config)
cg.add(var.set_interrupt_pin(interrupt_pin))
if reset_pin_config := config.get(CONF_RESET_PIN):
reset_pin = await cg.gpio_pin_expression(reset_pin_config)
cg.add(var.set_reset_pin(reset_pin))

View file

@ -0,0 +1,96 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display, font, color
from esphome.const import CONF_ID, CONF_TRIGGER_ID
from esphome import automation, core
from esphome.components.display_menu_base import (
DISPLAY_MENU_BASE_SCHEMA,
DisplayMenuComponent,
display_menu_to_code,
)
CONF_DISPLAY = "display"
CONF_FONT = "font"
CONF_MENU_ITEM_VALUE = "menu_item_value"
CONF_FOREGROUND_COLOR = "foreground_color"
CONF_BACKGROUND_COLOR = "background_color"
CONF_ON_REDRAW = "on_redraw"
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")
GraphicalDisplayMenu = graphical_display_menu_ns.class_(
"GraphicalDisplayMenu", DisplayMenuComponent
)
GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const")
MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments")
MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator(
"const"
)
GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_(
"GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger
)
CODEOWNERS = ["@MrMDavidson"]
AUTO_LOAD = ["display_menu_base"]
CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu),
cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer),
cv.Required(CONF_FONT): cv.use_id(font.Font),
cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string),
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_ON_REDRAW): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
GraphicalDisplayMenuOnRedrawTrigger
)
}
),
}
)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if display_config := config.get(CONF_DISPLAY):
drawing_display = await cg.get_variable(display_config)
cg.add(var.set_display(drawing_display))
menu_font = await cg.get_variable(config[CONF_FONT])
cg.add(var.set_font(menu_font))
if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None:
if isinstance(menu_item_value_config, core.Lambda):
template_ = await cg.templatable(
menu_item_value_config,
[(MenuItemValueArgumentsConstPtr, "it")],
cg.std_string,
)
cg.add(var.set_menu_item_value(template_))
else:
cg.add(var.set_menu_item_value(menu_item_value_config))
if foreground_color_config := config.get(CONF_FOREGROUND_COLOR):
foreground_color = await cg.get_variable(foreground_color_config)
cg.add(var.set_foreground_color(foreground_color))
if background_color_config := config.get(CONF_BACKGROUND_COLOR):
background_color = await cg.get_variable(background_color_config)
cg.add(var.set_background_color(background_color))
for conf in config.get(CONF_ON_REDRAW, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf
)
await display_menu_to_code(var, config)
cg.add_define("USE_GRAPHICAL_DISPLAY_MENU")

View file

@ -0,0 +1,243 @@
#include "graphical_display_menu.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstdlib>
#include "esphome/components/display/display.h"
namespace esphome {
namespace graphical_display_menu {
static const char *const TAG = "graphical_display_menu";
void GraphicalDisplayMenu::setup() {
if (this->display_ != nullptr) {
display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); };
this->display_page_ = make_unique<display::DisplayPage>(writer);
}
if (!this->menu_item_value_.has_value()) {
this->menu_item_value_ = [](const MenuItemValueArguments *it) {
std::string label = " ";
if (it->is_item_selected && it->is_menu_editing) {
label.append(">");
label.append(it->item->get_value_text());
label.append("<");
} else {
label.append("(");
label.append(it->item->get_value_text());
label.append(")");
}
return label;
};
}
display_menu_base::DisplayMenuComponent::setup();
}
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(TAG, "Graphical Display Menu");
ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr));
ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr));
ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr));
ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr));
ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick");
ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_));
ESP_LOGCONFIG(TAG, "Menu items:");
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
auto *item = this->displayed_item_->get_item(i);
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())),
YESNO(item->get_immediate_edit()));
}
}
void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; }
void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; }
void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }
void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; }
void GraphicalDisplayMenu::on_before_show() {
if (this->display_ != nullptr) {
this->previous_display_page_ = this->display_->get_active_page();
this->display_->show_page(this->display_page_.get());
this->display_->clear();
} else {
this->update();
}
}
void GraphicalDisplayMenu::on_before_hide() {
if (this->previous_display_page_ != nullptr) {
this->display_->show_page((display::DisplayPage *) this->previous_display_page_);
this->display_->clear();
this->update();
this->previous_display_page_ = nullptr;
} else {
this->update();
}
}
void GraphicalDisplayMenu::draw_and_update() {
this->update();
// If we're in advanced drawing mode we won't have a display and will instead require the update callback to do
// our drawing
if (this->display_ != nullptr) {
draw_menu();
}
}
void GraphicalDisplayMenu::draw_menu() {
if (this->display_ == nullptr) {
ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode");
return;
}
display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height());
this->draw_menu_internal_(this->display_, &bounds);
}
void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) {
this->draw_menu_internal_(display, bounds);
}
void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) {
int total_height = 0;
int y_padding = 2;
bool scroll_menu_items = false;
std::vector<display::Rect> menu_dimensions;
int number_items_fit_to_screen = 0;
const int max_item_index = this->displayed_item_->items_size() - 1;
for (size_t i = 0; i <= max_item_index; i++) {
const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_;
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
menu_dimensions.push_back(item_dimensions);
total_height += item_dimensions.h + (i == 0 ? 0 : y_padding);
if (total_height <= bounds->h) {
number_items_fit_to_screen++;
} else {
// Scroll the display if the selected item or the item immediately after it overflows
if ((selected) || (i == this->cursor_index_ + 1)) {
scroll_menu_items = true;
}
}
}
// Determine what items to draw
int first_item_index = 0;
int last_item_index = max_item_index;
if (number_items_fit_to_screen <= 1) {
// If only one item can fit to the bounds draw the current cursor item
last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
first_item_index = this->cursor_index_;
} else {
if (scroll_menu_items) {
// Attempt to draw the item after the current item (+1 for equality check in the draw loop)
last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
// Go back through the measurements to determine how many prior items we can fit
int height_left_to_use = bounds->h;
for (int i = last_item_index; i >= 0; i--) {
const display::Rect item_dimensions = menu_dimensions[i];
height_left_to_use -= (item_dimensions.h + y_padding);
if (height_left_to_use <= 0) {
// Ran out of space - this is our first item to draw
first_item_index = i;
break;
}
}
const int items_to_draw = last_item_index - first_item_index;
// Dont't draw last item partially if it is the selected item
if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) &&
(first_item_index < max_item_index)) {
first_item_index++;
}
}
}
// Render the items into the view port
display->start_clipping(*bounds);
int y_offset = bounds->y;
for (size_t i = first_item_index; i <= last_item_index; i++) {
const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_;
display::Rect dimensions = menu_dimensions[i];
dimensions.y = y_offset;
dimensions.x = bounds->x;
this->draw_item(display, item, &dimensions, selected);
y_offset = dimensions.y + dimensions.h + y_padding;
}
display->end_clipping();
}
display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, const bool selected) {
display::Rect dimensions(0, 0, 0, 0);
if (selected) {
// TODO: Support selection glyph
dimensions.w += 0;
dimensions.h += 0;
}
std::string label = item->get_text();
if (item->has_value()) {
// Append to label
MenuItemValueArguments args(item, selected, this->editing_);
label.append(this->menu_item_value_.value(&args));
}
int x1;
int y1;
int width;
int height;
display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height);
dimensions.w = std::min((int16_t) width, bounds->w);
dimensions.h = std::min((int16_t) height, bounds->h);
return dimensions;
}
inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, const bool selected) {
const auto background_color = selected ? this->foreground_color_ : this->background_color_;
const auto foreground_color = selected ? this->background_color_ : this->foreground_color_;
// int background_width = std::max(bounds->width, available_width);
int background_width = bounds->w;
if (selected) {
display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color);
}
std::string label = item->get_text();
if (item->has_value()) {
MenuItemValueArguments args(item, selected, this->editing_);
label.append(this->menu_item_value_.value(&args));
}
display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str());
}
void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) {
ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific "
"draw_item should be called.");
}
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); }
} // namespace graphical_display_menu
} // namespace esphome

View file

@ -0,0 +1,84 @@
#pragma once
#include "esphome/core/color.h"
#include "esphome/components/display_menu_base/display_menu_base.h"
#include "esphome/components/display_menu_base/menu_item.h"
#include "esphome/core/automation.h"
#include <cstdlib>
namespace esphome {
// forward declare from display namespace
namespace display {
class Display;
class DisplayPage;
class BaseFont;
class Rect;
} // namespace display
namespace graphical_display_menu {
const Color COLOR_ON(255, 255, 255, 255);
const Color COLOR_OFF(0, 0, 0, 0);
struct MenuItemValueArguments {
MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) {
this->item = item;
this->is_item_selected = is_item_selected;
this->is_menu_editing = is_menu_editing;
}
const display_menu_base::MenuItem *item;
bool is_item_selected;
bool is_menu_editing;
};
class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent {
public:
void setup() override;
void dump_config() override;
void set_display(display::Display *display);
void set_font(display::BaseFont *font);
template<typename V> void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; }
void set_foreground_color(Color foreground_color);
void set_background_color(Color background_color);
void add_on_redraw_callback(std::function<void()> &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); }
void draw(display::Display *display, const display::Rect *bounds);
protected:
void draw_and_update() override;
void draw_menu() override;
void draw_menu_internal_(display::Display *display, const display::Rect *bounds);
void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override;
virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, bool selected);
virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item,
const display::Rect *bounds, bool selected);
void update() override;
void on_before_show() override;
void on_before_hide() override;
std::unique_ptr<display::DisplayPage> display_page_{nullptr};
const display::DisplayPage *previous_display_page_{nullptr};
display::Display *display_{nullptr};
display::BaseFont *font_{nullptr};
TemplatableValue<std::string, const MenuItemValueArguments *> menu_item_value_;
Color foreground_color_{COLOR_ON};
Color background_color_{COLOR_OFF};
CallbackManager<void()> on_redraw_callbacks_{};
};
class GraphicalDisplayMenuOnRedrawTrigger : public Trigger<const GraphicalDisplayMenu *> {
public:
explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) {
parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); });
}
};
} // namespace graphical_display_menu
} // namespace esphome

View file

@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@jesserockz", "@clydebarrow"]
DEPENDENCIES = ["i2c"]
gt911_ns = cg.esphome_ns.namespace("gt911")

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_INDEX
from .. import gt911_ns
from ..touchscreen import GT911Touchscreen, GT911ButtonListener
CONF_GT911_ID = "gt911_id"
GT911Button = gt911_ns.class_(
"GT911Button",
binary_sensor.BinarySensor,
cg.Component,
GT911ButtonListener,
cg.Parented.template(GT911Touchscreen),
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(GT911Button).extend(
{
cv.GenerateID(CONF_GT911_ID): cv.use_id(GT911Touchscreen),
cv.Optional(CONF_INDEX, default=0): cv.int_range(min=0, max=3),
}
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_GT911_ID])
cg.add(var.set_index(config[CONF_INDEX]))

View file

@ -0,0 +1,27 @@
#include "gt911_button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gt911 {
static const char *const TAG = "GT911.binary_sensor";
void GT911Button::setup() {
this->parent_->register_button_listener(this);
this->publish_initial_state(false);
}
void GT911Button::dump_config() {
LOG_BINARY_SENSOR("", "GT911 Button", this);
ESP_LOGCONFIG(TAG, " Index: %u", this->index_);
}
void GT911Button::update_button(uint8_t index, bool state) {
if (index != this->index_)
return;
this->publish_state(state);
}
} // namespace gt911
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/gt911/touchscreen/gt911_touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace gt911 {
class GT911Button : public binary_sensor::BinarySensor,
public Component,
public GT911ButtonListener,
public Parented<GT911Touchscreen> {
public:
void setup() override;
void dump_config() override;
void set_index(uint8_t index) { this->index_ = index; }
void update_button(uint8_t index, bool state) override;
protected:
uint8_t index_;
};
} // namespace gt911
} // namespace esphome

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c, touchscreen
from esphome.const import CONF_INTERRUPT_PIN, CONF_ID
from .. import gt911_ns
GT911ButtonListener = gt911_ns.class_("GT911ButtonListener")
GT911Touchscreen = gt911_ns.class_(
"GT911Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GT911Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
).extend(i2c.i2c_device_schema(0x5D))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))

View file

@ -0,0 +1,111 @@
#include "gt911_touchscreen.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gt911 {
static const char *const TAG = "gt911.touchscreen";
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D};
static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48};
static const size_t MAX_TOUCHES = 5; // max number of possible touches reported
#define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \
ESP_LOGE(TAG, "Failed to communicate!"); \
this->status_set_warning(); \
return; \
}
void GT911Touchscreen::setup() {
i2c::ErrorCode err;
ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen...");
// check the configuration of the int line.
uint8_t data[4];
err = this->write(GET_SWITCHES, 2);
if (err == i2c::ERROR_OK) {
err = this->read(data, 1);
if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]);
if (this->interrupt_pin_ != nullptr) {
// datasheet says NOT to use pullup/down on the int line.
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
}
}
}
if (err == i2c::ERROR_OK) {
err = this->write(GET_MAX_VALUES, 2);
if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) {
this->x_raw_max_ = encode_uint16(data[1], data[0]);
this->y_raw_max_ = encode_uint16(data[3], data[2]);
esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_);
}
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to communicate!");
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
}
void GT911Touchscreen::update_touches() {
i2c::ErrorCode err;
uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false);
ERROR_CHECK(err);
err = this->read(&touch_state, 1);
ERROR_CHECK(err);
this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE));
uint8_t num_of_touches = touch_state & 0x07;
if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet.
return;
}
if (num_of_touches == 0)
return;
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
ERROR_CHECK(err);
// num_of_touches is guaranteed to be 0..5. Also read the key data
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
ERROR_CHECK(err);
for (uint8_t i = 0; i != num_of_touches; i++) {
uint16_t id = data[i][0];
uint16_t x = encode_uint16(data[i][2], data[i][1]);
uint16_t y = encode_uint16(data[i][4], data[i][3]);
this->set_raw_touch_position_(id, x, y);
}
auto keys = data[num_of_touches][0];
for (size_t i = 0; i != 4; i++) {
for (auto *listener : this->button_listeners_)
listener->update_button(i, (keys & (1 << i)) != 0);
}
}
void GT911Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
}
} // namespace gt911
} // namespace esphome

View file

@ -0,0 +1,32 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace gt911 {
class GT911ButtonListener {
public:
virtual void update_button(uint8_t index, bool state) = 0;
};
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); }
protected:
void update_touches() override;
InternalGPIOPin *interrupt_pin_{};
std::vector<GT911ButtonListener *> button_listeners_;
};
} // namespace gt911
} // namespace esphome

View file

@ -3,6 +3,7 @@ import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import fan, output
from esphome.components.fan import validate_preset_modes
from esphome.const import (
CONF_ID,
CONF_DECAY_MODE,
@ -10,6 +11,7 @@ from esphome.const import (
CONF_PIN_A,
CONF_PIN_B,
CONF_ENABLE_PIN,
CONF_PRESET_MODES,
)
from .. import hbridge_ns
@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = {
# Actions
BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan),
@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
).extend(cv.COMPONENT_SCHEMA)
@ -69,3 +71,6 @@ async def to_code(config):
if CONF_ENABLE_PIN in config:
enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
cg.add(var.set_enable_pin(enable_pin))
if CONF_PRESET_MODES in config:
cg.add(var.set_preset_modes(config[CONF_PRESET_MODES]))

View file

@ -33,7 +33,12 @@ void HBridgeFan::setup() {
restore->apply(*this);
this->write_state_();
}
// Construct traits
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
}
void HBridgeFan::dump_config() {
LOG_FAN("", "H-Bridge Fan", this);
if (this->decay_mode_ == DECAY_MODE_SLOW) {
@ -42,9 +47,7 @@ void HBridgeFan::dump_config() {
ESP_LOGCONFIG(TAG, " Decay Mode: Fast");
}
}
fan::FanTraits HBridgeFan::get_traits() {
return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
}
void HBridgeFan::control(const fan::FanCall &call) {
if (call.get_state().has_value())
this->state = *call.get_state();
@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) {
this->oscillating = *call.get_oscillating();
if (call.get_direction().has_value())
this->direction = *call.get_direction();
this->preset_mode = call.get_preset_mode();
this->write_state_();
this->publish_state();
}
void HBridgeFan::write_state_() {
float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f;
if (speed == 0.0f) { // off means idle

View file

@ -1,5 +1,7 @@
#pragma once
#include <set>
#include "esphome/core/automation.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan {
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; }
void setup() override;
void dump_config() override;
fan::FanTraits get_traits() override;
fan::FanTraits get_traits() override { return this->traits_; }
fan::FanCall brake();
@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan {
output::BinaryOutput *oscillating_{nullptr};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::set<std::string> preset_modes_{};
void control(const fan::FanCall &call) override;
void write_state_();

View file

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

View file

@ -0,0 +1,47 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import cover, uart
from esphome.const import (
CONF_CLOSE_DURATION,
CONF_ID,
CONF_OPEN_DURATION,
)
he60r_ns = cg.esphome_ns.namespace("he60r")
HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component)
CONFIG_SCHEMA = (
cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{
cv.GenerateID(): cv.declare_id(HE60rCover),
cv.Optional(
CONF_OPEN_DURATION, default="15s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_CLOSE_DURATION, default="15s"
): cv.positive_time_period_milliseconds,
}
)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"he60r",
baud_rate=1200,
require_tx=True,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cover.register_cover(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))

View file

@ -0,0 +1,265 @@
#include "he60r.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace he60r {
static const char *const TAG = "he60r.cover";
static const uint8_t QUERY_BYTE = 0x38;
static const uint8_t TOGGLE_BYTE = 0x30;
using namespace esphome::cover;
void HE60rCover::setup() {
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
this->publish_state(false);
} else {
// if no other information, assume half open
this->position = 0.5f;
}
this->current_operation = COVER_OPERATION_IDLE;
this->last_recompute_time_ = this->start_dir_time_ = millis();
this->set_interval(300, [this]() { this->update_(); });
}
CoverTraits HE60rCover::get_traits() {
auto traits = CoverTraits();
traits.set_supports_stop(true);
traits.set_supports_position(true);
traits.set_supports_toggle(true);
traits.set_is_assumed_state(false);
return traits;
}
void HE60rCover::dump_config() {
LOG_COVER("", "HE60R Cover", this);
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
auto restore = this->restore_state_();
if (restore.has_value())
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
}
void HE60rCover::endstop_reached_(CoverOperation operation) {
const uint32_t now = millis();
this->set_current_operation_(COVER_OPERATION_IDLE);
auto new_position = operation == COVER_OPERATION_OPENING ? COVER_OPEN : COVER_CLOSED;
if (new_position != this->position || this->current_operation != COVER_OPERATION_IDLE) {
this->position = new_position;
this->current_operation = COVER_OPERATION_IDLE;
if (this->last_command_ == operation) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
}
this->publish_state();
}
}
void HE60rCover::set_current_operation_(cover::CoverOperation operation) {
if (this->current_operation != operation) {
this->current_operation = operation;
if (operation != COVER_OPERATION_IDLE)
this->last_recompute_time_ = millis();
this->publish_state();
}
}
void HE60rCover::process_rx_(uint8_t data) {
ESP_LOGV(TAG, "Process RX data %X", data);
if (!this->query_seen_) {
this->query_seen_ = data == QUERY_BYTE;
if (!this->query_seen_)
ESP_LOGD(TAG, "RX Byte %02X", data);
return;
}
switch (data) {
case 0xB5: // at closed endstop, jammed?
case 0xF5: // at closed endstop, jammed?
case 0x55: // at closed endstop
this->next_direction_ = COVER_OPERATION_OPENING;
this->endstop_reached_(COVER_OPERATION_CLOSING);
break;
case 0x52: // at opened endstop
this->next_direction_ = COVER_OPERATION_CLOSING;
this->endstop_reached_(COVER_OPERATION_OPENING);
break;
case 0x51: // travelling up after encountering obstacle
case 0x01: // travelling up
case 0x11: // travelling up, triggered by remote
this->set_current_operation_(COVER_OPERATION_OPENING);
this->next_direction_ = COVER_OPERATION_IDLE;
break;
case 0x44: // travelling down
case 0x14: // travelling down, triggered by remote
this->next_direction_ = COVER_OPERATION_IDLE;
this->set_current_operation_(COVER_OPERATION_CLOSING);
break;
case 0x86: // Stopped, jammed?
case 0x16: // stopped midway while opening, by remote
case 0x06: // stopped midway while opening
this->next_direction_ = COVER_OPERATION_CLOSING;
this->set_current_operation_(COVER_OPERATION_IDLE);
break;
case 0x10: // stopped midway while closing, by remote
case 0x00: // stopped midway while closing
this->next_direction_ = COVER_OPERATION_OPENING;
this->set_current_operation_(COVER_OPERATION_IDLE);
break;
default:
break;
}
}
void HE60rCover::update_() {
if (toggles_needed_ != 0) {
if ((this->counter_++ & 0x3) == 0) {
toggles_needed_--;
ESP_LOGD(TAG, "Writing byte 0x30, still needed=%d", toggles_needed_);
this->write_byte(TOGGLE_BYTE);
} else {
this->write_byte(QUERY_BYTE);
}
} else {
this->write_byte(QUERY_BYTE);
this->counter_ = 0;
}
if (this->current_operation != COVER_OPERATION_IDLE) {
this->recompute_position_();
// if we initiated the move, check if we reached the target position
if (this->last_command_ != COVER_OPERATION_IDLE) {
if (this->is_at_target_()) {
this->start_direction_(COVER_OPERATION_IDLE);
}
}
}
}
void HE60rCover::loop() {
uint8_t data;
while (this->available() > 0) {
if (this->read_byte(&data)) {
this->process_rx_(data);
}
}
}
void HE60rCover::control(const CoverCall &call) {
if (call.get_stop()) {
this->start_direction_(COVER_OPERATION_IDLE);
} else if (call.get_toggle().has_value()) {
// toggle action logic: OPEN - STOP - CLOSE
if (this->last_command_ != COVER_OPERATION_IDLE) {
this->start_direction_(COVER_OPERATION_IDLE);
} else {
this->toggles_needed_++;
}
} else if (call.get_position().has_value()) {
// go to position action
auto pos = *call.get_position();
// are we at the target?
if (pos == this->position) {
this->start_direction_(COVER_OPERATION_IDLE);
} else {
this->target_position_ = pos;
this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
}
}
}
/**
* Check if the cover has reached or passed the target position. This is used only
* for partial open/close requests - endstops are used for full open/close.
* @return True if the cover has reached or passed its target position. For full open/close target always return false.
*/
bool HE60rCover::is_at_target_() const {
// equality of floats is fraught with peril - this is reliable since the values are 0.0 or 1.0 which are
// exactly representable.
if (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)
return false;
// aiming for an intermediate position - exact comparison here will not work and we need to allow for overshoot
switch (this->last_command_) {
case COVER_OPERATION_OPENING:
return this->position >= this->target_position_;
case COVER_OPERATION_CLOSING:
return this->position <= this->target_position_;
case COVER_OPERATION_IDLE:
return this->current_operation == COVER_OPERATION_IDLE;
default:
return true;
}
}
void HE60rCover::start_direction_(CoverOperation dir) {
this->last_command_ = dir;
if (this->current_operation == dir)
return;
ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
if (dir == this->next_direction_) {
// either moving and needs to stop, or stopped and will move correctly on one trigger
this->toggles_needed_ = 1;
} else {
if (this->current_operation == COVER_OPERATION_IDLE) {
// if stopped, but will go the wrong way, need 3 triggers.
this->toggles_needed_ = 3;
} else {
// just stop and reverse
this->toggles_needed_ = 2;
}
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
}
this->start_dir_time_ = millis();
}
void HE60rCover::recompute_position_() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
float dir;
float action_dur;
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
dir = 1.0f;
action_dur = this->open_duration_;
break;
case COVER_OPERATION_CLOSING:
dir = -1.0f;
action_dur = this->close_duration_;
break;
default:
return;
}
if (now > this->last_recompute_time_) {
auto diff = now - last_recompute_time_;
auto delta = dir * diff / action_dur;
// make sure our guesstimate never reaches full open or close.
this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f);
ESP_LOGD(TAG, "Recompute %dms, dir=%f, action_dur=%f, delta=%f, pos=%f", (int) diff, dir, action_dur, delta,
this->position);
this->last_recompute_time_ = now;
this->publish_state();
}
}
} // namespace he60r
} // namespace esphome

View file

@ -0,0 +1,47 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/cover/cover.h"
namespace esphome {
namespace he60r {
class HE60rCover : public cover::Cover, public Component, public uart::UARTDevice {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; };
void set_open_duration(uint32_t duration) { this->open_duration_ = duration; }
void set_close_duration(uint32_t duration) { this->close_duration_ = duration; }
cover::CoverTraits get_traits() override;
protected:
void update_();
void control(const cover::CoverCall &call) override;
bool is_at_target_() const;
void start_direction_(cover::CoverOperation dir);
void update_operation_(cover::CoverOperation dir);
void endstop_reached_(cover::CoverOperation operation);
void recompute_position_();
void set_current_operation_(cover::CoverOperation operation);
void process_rx_(uint8_t data);
uint32_t open_duration_{0};
uint32_t close_duration_{0};
uint32_t toggles_needed_{0};
cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE};
cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE};
uint32_t last_recompute_time_{0};
uint32_t start_dir_time_{0};
float target_position_{0};
bool query_seen_{};
uint8_t counter_{};
};
} // namespace he60r
} // namespace esphome

View file

@ -17,10 +17,8 @@ import esphome.codegen as cg
from .const import host_ns
_LOGGER = logging.getLogger(__name__)
HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin)
@ -45,21 +43,10 @@ def validate_gpio_pin(value):
return _translate_pin(value)
HOST_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(HostGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
},
HOST_PIN_SCHEMA = pins.gpio_base_schema(
HostGPIOPin,
validate_gpio_pin,
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN, CONF_PULLUP, CONF_PULLDOWN],
)

View file

@ -39,9 +39,8 @@ def _bus_declare_type(value):
raise NotImplementedError
pin_with_input_and_output_support = cv.All(
pins.internal_gpio_pin_number({CONF_INPUT: True}),
pins.internal_gpio_pin_number({CONF_OUTPUT: True}),
pin_with_input_and_output_support = pins.internal_gpio_pin_number(
{CONF_OUTPUT: True, CONF_INPUT: True}
)

View file

@ -220,6 +220,8 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) {
return index;
}
bool I2SAudioSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; }
} // namespace i2s_audio
} // namespace esphome

View file

@ -56,6 +56,8 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
size_t play(const uint8_t *data, size_t length) override;
bool has_buffered_data() const override;
protected:
void start_();
// void stop_();

View file

@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import core, pins
from esphome.components import display, spi, font
from esphome.components.display import validate_rotation
from esphome.core import CORE, HexInt
from esphome.const import (
CONF_COLOR_PALETTE,
@ -13,6 +14,9 @@ from esphome.const import (
CONF_PAGES,
CONF_RESET_PIN,
CONF_DIMENSIONS,
CONF_WIDTH,
CONF_HEIGHT,
CONF_ROTATION,
)
DEPENDENCIES = ["spi"]
@ -26,28 +30,39 @@ def AUTO_LOAD():
CODEOWNERS = ["@nielsnl68", "@clydebarrow"]
ili9XXX_ns = cg.esphome_ns.namespace("ili9xxx")
ili9XXXSPI = ili9XXX_ns.class_(
"ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx")
ILI9XXXDisplay = ili9xxx_ns.class_(
"ILI9XXXDisplay",
cg.PollingComponent,
spi.SPIDevice,
display.Display,
display.DisplayBuffer,
)
ILI9XXXColorMode = ili9XXX_ns.enum("ILI9XXXColorMode")
ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode")
ColorOrder = display.display_ns.enum("ColorMode")
MODELS = {
"M5STACK": ili9XXX_ns.class_("ILI9XXXM5Stack", ili9XXXSPI),
"M5CORE": ili9XXX_ns.class_("ILI9XXXM5CORE", ili9XXXSPI),
"TFT_2.4": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI),
"TFT_2.4R": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI),
"ILI9341": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI),
"ILI9342": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI),
"ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI),
"ILI9481-18": ili9XXX_ns.class_("ILI9XXXILI948118", ili9XXXSPI),
"ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI),
"ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI),
"ILI9488_A": ili9XXX_ns.class_("ILI9XXXILI9488A", ili9XXXSPI),
"ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI),
"S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI),
"S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI),
"M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay),
"M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay),
"TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay),
"TFT_2.4R": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay),
"ILI9341": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay),
"ILI9342": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay),
"ILI9481": ili9xxx_ns.class_("ILI9XXXILI9481", ILI9XXXDisplay),
"ILI9481-18": ili9xxx_ns.class_("ILI9XXXILI948118", ILI9XXXDisplay),
"ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay),
"ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay),
"ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay),
"ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay),
"ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay),
"S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay),
"S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay),
}
COLOR_ORDERS = {
"RGB": ColorOrder.COLOR_ORDER_RGB,
"BGR": ColorOrder.COLOR_ORDER_BGR,
}
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
@ -55,6 +70,14 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
CONF_LED_PIN = "led_pin"
CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
CONF_INVERT_DISPLAY = "invert_display"
CONF_INVERT_COLORS = "invert_colors"
CONF_MIRROR_X = "mirror_x"
CONF_MIRROR_Y = "mirror_y"
CONF_SWAP_XY = "swap_xy"
CONF_COLOR_ORDER = "color_order"
CONF_OFFSET_HEIGHT = "offset_height"
CONF_OFFSET_WIDTH = "offset_width"
CONF_TRANSFORM = "transform"
def _validate(config):
@ -77,6 +100,7 @@ def _validate(config):
"TFT_2.4R",
"ILI9341",
"ILI9342",
"ST7789V",
]:
raise cv.Invalid(
"Provided model can't run on ESP8266. Use an ESP32 with PSRAM onboard"
@ -88,9 +112,19 @@ CONFIG_SCHEMA = cv.All(
font.validate_pillow_installed,
display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(ili9XXXSPI),
cv.GenerateID(): cv.declare_id(ILI9XXXDisplay),
cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True, space="_"),
cv.Optional(CONF_DIMENSIONS): cv.dimensions,
cv.Optional(CONF_DIMENSIONS): cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_,
cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_,
}
),
),
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_LED_PIN): cv.invalid(
@ -101,7 +135,19 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list(
cv.file_
),
cv.Optional(CONF_INVERT_DISPLAY): cv.boolean,
cv.Optional(CONF_INVERT_DISPLAY): cv.invalid(
"'invert_display' has been replaced by 'invert_colors'"
),
cv.Optional(CONF_INVERT_COLORS): cv.boolean,
cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True),
cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema(
{
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
}
),
}
)
.extend(cv.polling_component_schema("1s"))
@ -115,11 +161,17 @@ async def to_code(config):
rhs = MODELS[config[CONF_MODEL]].new()
var = cg.Pvariable(config[CONF_ID], rhs)
await cg.register_component(var, config)
await display.register_display(var, config)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))
if CONF_COLOR_ORDER in config:
cg.add(var.set_color_order(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
if CONF_TRANSFORM in config:
transform = config[CONF_TRANSFORM]
cg.add(var.set_swap_xy(transform[CONF_SWAP_XY]))
cg.add(var.set_mirror_x(transform[CONF_MIRROR_X]))
cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y]))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
@ -132,9 +184,17 @@ async def to_code(config):
cg.add(var.set_reset_pin(reset))
if CONF_DIMENSIONS in config:
cg.add(
var.set_dimentions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])
)
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
cg.add(
var.set_offsets(
dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT]
)
)
else:
(width, height) = dimensions
cg.add(var.set_dimensions(width, height))
rhs = None
if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
@ -179,5 +239,5 @@ async def to_code(config):
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_palette(prog_arr))
if CONF_INVERT_DISPLAY in config:
cg.add(var.invert_display(config[CONF_INVERT_DISPLAY]))
if CONF_INVERT_COLORS in config:
cg.add(var.invert_colors(config[CONF_INVERT_COLORS]))

View file

@ -8,11 +8,31 @@ namespace esphome {
namespace ili9xxx {
static const char *const TAG = "ili9xxx";
static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write
static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer
// store a 16 bit value in a buffer, big endian.
static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
buf[1] = value;
}
void ILI9XXXDisplay::setup() {
ESP_LOGD(TAG, "Setting up ILI9xxx");
this->setup_pins_();
this->initialize();
this->command(this->pre_invertdisplay_ ? ILI9XXX_INVON : ILI9XXX_INVOFF);
this->init_lcd_();
this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF);
// custom x/y transform and color order
uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB;
if (this->swap_xy_)
mad |= MADCTL_MV;
if (this->mirror_x_)
mad |= MADCTL_MX;
if (this->mirror_y_)
mad |= MADCTL_MY;
this->send_command(ILI9XXX_MADCTL, &mad, 1);
this->x_low_ = this->width_;
this->y_low_ = this->height_;
@ -47,6 +67,8 @@ void ILI9XXXDisplay::setup_pins_() {
void ILI9XXXDisplay::dump_config() {
LOG_DISPLAY("", "ili9xxx", this);
ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_x_);
ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_y_);
switch (this->buffer_color_mode_) {
case BITS_8_INDEXED:
ESP_LOGCONFIG(TAG, " Color mode: 8bit Indexed");
@ -64,8 +86,12 @@ void ILI9XXXDisplay::dump_config() {
ESP_LOGCONFIG(TAG, " Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_));
ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_));
ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_));
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!");
@ -141,12 +167,14 @@ void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color)
}
if (updated) {
// low and high watermark may speed up drawing from buffer
this->x_low_ = (x < this->x_low_) ? x : this->x_low_;
this->y_low_ = (y < this->y_low_) ? y : this->y_low_;
this->x_high_ = (x > this->x_high_) ? x : this->x_high_;
this->y_high_ = (y > this->y_high_) ? y : this->y_high_;
// ESP_LOGVV(TAG, "=>>> pixel (x:%d, y:%d) (xl:%d, xh:%d, yl:%d, yh:%d", x, y, this->x_low_, this->x_high_,
// this->y_low_, this->y_high_);
if (x < this->x_low_)
this->x_low_ = x;
if (y < this->y_low_)
this->y_low_ = y;
if (x > this->x_high_)
this->x_high_ = x;
if (y > this->y_high_)
this->y_high_ = y;
}
}
@ -165,59 +193,82 @@ void ILI9XXXDisplay::update() {
}
void ILI9XXXDisplay::display_() {
// we will only update the changed window to the display
uint16_t w = this->x_high_ - this->x_low_ + 1; // NOLINT
uint16_t h = this->y_high_ - this->y_low_ + 1; // NOLINT
uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_);
uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE];
// check if something was displayed
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
ESP_LOGV(TAG, "Nothing to display");
return;
}
set_addr_window_(this->x_low_, this->y_low_, w, h);
// we will only update the changed rows to the display
size_t const w = this->x_high_ - this->x_low_ + 1;
size_t const h = this->y_high_ - this->y_low_ + 1;
size_t mhz = this->data_rate_ / 1000000;
// estimate time for a single write
size_t sw_time = this->width_ * h * 16 / mhz + this->width_ * h * 2 / SPI_MAX_BLOCK_SIZE * SPI_SETUP_US * 2;
// estimate time for multiple writes
size_t mw_time = (w * h * 16) / mhz + w * h * 2 / ILI9XXX_TRANSFER_BUFFER_SIZE * SPI_SETUP_US;
ESP_LOGV(TAG,
"Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, "
"heigth:%d, start_pos:%" PRId32 ")",
this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, start_pos);
this->start_data_();
for (uint16_t row = 0; row < h; row++) {
uint32_t pos = start_pos + (row * width_);
uint32_t rem = w;
while (rem > 0) {
uint32_t sz = std::min(rem, ILI9XXX_TRANSFER_BUFFER_SIZE);
// ESP_LOGVV(TAG, "Send to display(pos:%d, rem:%d, zs:%d)", pos, rem, sz);
buffer_to_transfer_(pos, sz);
if (this->is_18bitdisplay_) {
for (uint32_t i = 0; i < sz; ++i) {
uint16_t color_val = transfer_buffer_[i];
uint8_t red = color_val & 0x1F;
uint8_t green = (color_val & 0x7E0) >> 5;
uint8_t blue = (color_val & 0xF800) >> 11;
uint8_t pass_buff[3];
pass_buff[2] = (uint8_t) ((red / 32.0) * 64) << 2;
pass_buff[1] = (uint8_t) green << 2;
pass_buff[0] = (uint8_t) ((blue / 32.0) * 64) << 2;
this->write_array(pass_buff, sizeof(pass_buff));
}
} else {
this->write_array16(transfer_buffer_, sz);
"height:%d, mode=%d, 18bit=%d, sw_time=%dus, mw_time=%dus)",
this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, this->buffer_color_mode_,
this->is_18bitdisplay_, sw_time, mw_time);
auto now = millis();
this->enable();
if (this->buffer_color_mode_ == BITS_16 && !this->is_18bitdisplay_ && sw_time < mw_time) {
// 16 bit mode maps directly to display format
ESP_LOGV(TAG, "Doing single write of %d bytes", this->width_ * h * 2);
set_addr_window_(0, this->y_low_, this->width_ - 1, this->y_high_);
this->write_array(this->buffer_ + this->y_low_ * this->width_ * 2, h * this->width_ * 2);
} else {
ESP_LOGV(TAG, "Doing multiple write");
size_t rem = h * w; // remaining number of pixels to write
set_addr_window_(this->x_low_, this->y_low_, this->x_high_, this->y_high_);
size_t idx = 0; // index into transfer_buffer
size_t pixel = 0; // pixel number offset
size_t pos = this->y_low_ * this->width_ + this->x_low_;
while (rem-- != 0) {
uint16_t color_val;
switch (this->buffer_color_mode_) {
case BITS_8:
color_val = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos++]));
break;
case BITS_8_INDEXED:
color_val = display::ColorUtil::color_to_565(
display::ColorUtil::index8_to_color_palette888(this->buffer_[pos++], this->palette_));
break;
default: // case BITS_16:
color_val = (buffer_[pos * 2] << 8) + buffer_[pos * 2 + 1];
pos++;
break;
}
if (this->is_18bitdisplay_) {
transfer_buffer[idx++] = (uint8_t) ((color_val & 0xF800) >> 8); // Blue
transfer_buffer[idx++] = (uint8_t) ((color_val & 0x7E0) >> 3); // Green
transfer_buffer[idx++] = (uint8_t) (color_val << 3); // Red
} else {
put16_be(transfer_buffer + idx, color_val);
idx += 2;
}
if (idx == ILI9XXX_TRANSFER_BUFFER_SIZE) {
this->write_array(transfer_buffer, idx);
idx = 0;
App.feed_wdt();
}
// end of line? Skip to the next.
if (++pixel == w) {
pixel = 0;
pos += this->width_ - w;
}
pos += sz;
rem -= sz;
}
App.feed_wdt();
// flush any balance.
if (idx != 0) {
this->write_array(transfer_buffer, idx);
}
}
this->end_data_();
this->disable();
ESP_LOGV(TAG, "Data write took %dms", (unsigned) (millis() - now));
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
@ -225,26 +276,6 @@ void ILI9XXXDisplay::display_() {
this->y_high_ = 0;
}
uint32_t ILI9XXXDisplay::buffer_to_transfer_(uint32_t pos, uint32_t sz) {
for (uint32_t i = 0; i < sz; ++i) {
switch (this->buffer_color_mode_) {
case BITS_8_INDEXED:
transfer_buffer_[i] = display::ColorUtil::color_to_565(
display::ColorUtil::index8_to_color_palette888(this->buffer_[pos + i], this->palette_));
break;
case BITS_16:
transfer_buffer_[i] = ((uint16_t) this->buffer_[(pos + i) * 2] << 8) | this->buffer_[((pos + i) * 2) + 1];
continue;
break;
default:
transfer_buffer_[i] =
display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos + i]));
break;
}
}
return sz;
}
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
// values per bit is huge
uint32_t ILI9XXXDisplay::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); }
@ -303,11 +334,11 @@ void ILI9XXXDisplay::reset_() {
}
}
void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) {
void ILI9XXXDisplay::init_lcd_() {
uint8_t cmd, x, num_args;
const uint8_t *addr = init_cmd;
while ((cmd = progmem_read_byte(addr++)) > 0) {
x = progmem_read_byte(addr++);
const uint8_t *addr = this->init_sequence_;
while ((cmd = *addr++) > 0) {
x = *addr++;
num_args = x & 0x7F;
send_command(cmd, addr, num_args);
addr += num_args;
@ -316,27 +347,29 @@ void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) {
}
}
void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) {
uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1);
this->command(ILI9XXX_CASET); // Column address set
this->start_data_();
this->write_byte(x1 >> 8);
this->write_byte(x1);
this->write_byte(x2 >> 8);
this->write_byte(x2);
this->end_data_();
this->command(ILI9XXX_PASET); // Row address set
this->start_data_();
this->write_byte(y1 >> 8);
this->write_byte(y1);
this->write_byte(y2 >> 8);
this->write_byte(y2);
this->end_data_();
this->command(ILI9XXX_RAMWR); // Write to RAM
// Tell the display controller where we want to draw pixels.
// when called, the SPI should have already been enabled, only the D/C pin will be toggled here.
void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
uint8_t buf[4];
this->dc_pin_->digital_write(false);
this->write_byte(ILI9XXX_CASET); // Column address set
put16_be(buf, x1 + this->offset_x_);
put16_be(buf + 2, x2 + this->offset_x_);
this->dc_pin_->digital_write(true);
this->write_array(buf, sizeof buf);
this->dc_pin_->digital_write(false);
this->write_byte(ILI9XXX_PASET); // Row address set
put16_be(buf, y1 + this->offset_y_);
put16_be(buf + 2, y2 + this->offset_y_);
this->dc_pin_->digital_write(true);
this->write_array(buf, sizeof buf);
this->dc_pin_->digital_write(false);
this->write_byte(ILI9XXX_RAMWR); // Write to RAM
this->dc_pin_->digital_write(true);
}
void ILI9XXXDisplay::invert_display(bool invert) {
this->pre_invertdisplay_ = invert;
void ILI9XXXDisplay::invert_colors(bool invert) {
this->pre_invertcolors_ = invert;
if (is_ready()) {
this->command(invert ? ILI9XXX_INVON : ILI9XXX_INVOFF);
}
@ -345,132 +378,5 @@ void ILI9XXXDisplay::invert_display(bool invert) {
int ILI9XXXDisplay::get_width_internal() { return this->width_; }
int ILI9XXXDisplay::get_height_internal() { return this->height_; }
// M5Stack display
void ILI9XXXM5Stack::initialize() {
this->init_lcd_(INITCMD_M5STACK);
if (this->width_ == 0)
this->width_ = 320;
if (this->height_ == 0)
this->height_ = 240;
this->pre_invertdisplay_ = true;
}
// M5CORE display // Based on the configuration settings of M5stact's M5GFX code.
void ILI9XXXM5CORE::initialize() {
this->init_lcd_(INITCMD_M5CORE);
if (this->width_ == 0)
this->width_ = 320;
if (this->height_ == 0)
this->height_ = 240;
this->pre_invertdisplay_ = true;
}
// 24_TFT display
void ILI9XXXILI9341::initialize() {
this->init_lcd_(INITCMD_ILI9341);
if (this->width_ == 0)
this->width_ = 240;
if (this->height_ == 0)
this->height_ = 320;
}
// 24_TFT rotated display
void ILI9XXXILI9342::initialize() {
this->init_lcd_(INITCMD_ILI9341);
if (this->width_ == 0) {
this->width_ = 320;
}
if (this->height_ == 0) {
this->height_ = 240;
}
}
// 35_TFT display
void ILI9XXXILI9481::initialize() {
this->init_lcd_(INITCMD_ILI9481);
if (this->width_ == 0) {
this->width_ = 480;
}
if (this->height_ == 0) {
this->height_ = 320;
}
}
void ILI9XXXILI948118::initialize() {
this->init_lcd_(INITCMD_ILI9481_18);
if (this->width_ == 0) {
this->width_ = 320;
}
if (this->height_ == 0) {
this->height_ = 480;
}
this->is_18bitdisplay_ = true;
}
// 35_TFT display
void ILI9XXXILI9486::initialize() {
this->init_lcd_(INITCMD_ILI9486);
if (this->width_ == 0) {
this->width_ = 480;
}
if (this->height_ == 0) {
this->height_ = 320;
}
}
// 40_TFT display
void ILI9XXXILI9488::initialize() {
this->init_lcd_(INITCMD_ILI9488);
if (this->width_ == 0) {
this->width_ = 480;
}
if (this->height_ == 0) {
this->height_ = 320;
}
this->is_18bitdisplay_ = true;
}
// 40_TFT display
void ILI9XXXILI9488A::initialize() {
this->init_lcd_(INITCMD_ILI9488_A);
if (this->width_ == 0) {
this->width_ = 480;
}
if (this->height_ == 0) {
this->height_ = 320;
}
this->is_18bitdisplay_ = true;
}
// 40_TFT display
void ILI9XXXST7796::initialize() {
this->init_lcd_(INITCMD_ST7796);
if (this->width_ == 0) {
this->width_ = 320;
}
if (this->height_ == 0) {
this->height_ = 480;
}
}
// 24_TFT rotated display
void ILI9XXXS3Box::initialize() {
this->init_lcd_(INITCMD_S3BOX);
if (this->width_ == 0) {
this->width_ = 320;
}
if (this->height_ == 0) {
this->height_ = 240;
}
}
// 24_TFT rotated display
void ILI9XXXS3BoxLite::initialize() {
this->init_lcd_(INITCMD_S3BOXLITE);
if (this->width_ == 0) {
this->width_ = 320;
}
if (this->height_ == 0) {
this->height_ = 240;
}
this->pre_invertdisplay_ = true;
}
} // namespace ili9xxx
} // namespace esphome

View file

@ -7,7 +7,7 @@
namespace esphome {
namespace ili9xxx {
const uint32_t ILI9XXX_TRANSFER_BUFFER_SIZE = 64;
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
enum ILI9XXXColorMode {
BITS_8 = 0x08,
@ -19,25 +19,62 @@ enum ILI9XXXColorMode {
#define ILI9XXXDisplay_DATA_RATE spi::DATA_RATE_40MHZ
#endif // ILI9XXXDisplay_DATA_RATE
class ILI9XXXDisplay : public PollingComponent,
public display::DisplayBuffer,
class ILI9XXXDisplay : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, ILI9XXXDisplay_DATA_RATE> {
public:
ILI9XXXDisplay() = default;
ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height, bool invert_colors)
: init_sequence_{init_sequence}, width_{width}, height_{height}, pre_invertcolors_{invert_colors} {
uint8_t cmd, num_args, bits;
const uint8_t *addr = init_sequence;
while ((cmd = *addr++) != 0) {
num_args = *addr++ & 0x7F;
bits = *addr;
switch (cmd) {
case ILI9XXX_MADCTL: {
this->swap_xy_ = (bits & MADCTL_MV) != 0;
this->mirror_x_ = (bits & MADCTL_MX) != 0;
this->mirror_y_ = (bits & MADCTL_MY) != 0;
this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
break;
}
case ILI9XXX_PIXFMT: {
if ((bits & 0xF) == 6)
this->is_18bitdisplay_ = true;
break;
}
default:
break;
}
addr += num_args;
}
}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_palette(const uint8_t *palette) { this->palette_ = palette; }
void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; }
void set_dimentions(int16_t width, int16_t height) {
void set_dimensions(int16_t width, int16_t height) {
this->height_ = height;
this->width_ = width;
}
void invert_display(bool invert);
void set_offsets(int16_t offset_x, int16_t offset_y) {
this->offset_x_ = offset_x;
this->offset_y_ = offset_y;
}
void invert_colors(bool invert);
void command(uint8_t value);
void data(uint8_t value);
void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes);
uint8_t read_command(uint8_t command_byte, uint8_t index);
void set_color_order(display::ColorOrder color_order) { this->color_order_ = color_order; }
void set_swap_xy(bool swap_xy) { this->swap_xy_ = swap_xy; }
void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; }
void update() override;
@ -51,16 +88,17 @@ class ILI9XXXDisplay : public PollingComponent,
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void setup_pins_();
virtual void initialize() = 0;
void display_();
void init_lcd_(const uint8_t *init_cmd);
void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void init_lcd_();
void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2);
void reset_();
uint8_t const *init_sequence_{};
int16_t width_{0}; ///< Display width as modified by current rotation
int16_t height_{0}; ///< Display height as modified by current rotation
int16_t offset_x_{0};
int16_t offset_y_{0};
uint16_t x_low_{0};
uint16_t y_low_{0};
uint16_t x_high_{0};
@ -78,10 +116,6 @@ class ILI9XXXDisplay : public PollingComponent,
void start_data_();
void end_data_();
uint16_t transfer_buffer_[ILI9XXX_TRANSFER_BUFFER_SIZE];
uint32_t buffer_to_transfer_(uint32_t pos, uint32_t sz);
GPIOPin *reset_pin_{nullptr};
GPIOPin *dc_pin_{nullptr};
GPIOPin *busy_pin_{nullptr};
@ -89,77 +123,87 @@ class ILI9XXXDisplay : public PollingComponent,
bool prossing_update_ = false;
bool need_update_ = false;
bool is_18bitdisplay_ = false;
bool pre_invertdisplay_ = false;
bool pre_invertcolors_ = false;
display::ColorOrder color_order_{};
bool swap_xy_{};
bool mirror_x_{};
bool mirror_y_{};
};
//----------- M5Stack display --------------
class ILI9XXXM5Stack : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240, true) {}
};
//----------- M5Stack display --------------
class ILI9XXXM5CORE : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240, true) {}
};
//----------- ST7789V display --------------
class ILI9XXXST7789V : public ILI9XXXDisplay {
public:
ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320, false) {}
};
//----------- ILI9XXX_24_TFT display --------------
class ILI9XXXILI9341 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320, false) {}
};
//----------- ILI9XXX_24_TFT rotated display --------------
class ILI9XXXILI9342 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240, false) {}
};
//----------- ILI9XXX_??_TFT rotated display --------------
class ILI9XXXILI9481 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320, false) {}
};
//----------- ILI9481 in 18 bit mode --------------
class ILI9XXXILI948118 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480, true) {}
};
//----------- ILI9XXX_35_TFT rotated display --------------
class ILI9XXXILI9486 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {}
};
//----------- ILI9XXX_35_TFT rotated display --------------
class ILI9XXXILI9488 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI9488() : ILI9XXXDisplay(INITCMD_ILI9488, 480, 320, true) {}
};
//----------- ILI9XXX_35_TFT origin colors rotated display --------------
class ILI9XXXILI9488A : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320, true) {}
};
//----------- ILI9XXX_35_TFT rotated display --------------
class ILI9XXXST7796 : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480, false) {}
};
class ILI9XXXS3Box : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240, false) {}
};
class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
protected:
void initialize() override;
public:
ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {}
};
} // namespace ili9xxx

View file

@ -289,6 +289,33 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = {
0x00 // End of list
};
static const uint8_t PROGMEM INITCMD_ST7789V[] = {
ILI9XXX_SLPOUT , 0x80, // Exit Sleep
ILI9XXX_DISPON , 0x80, // Display on
ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR
ILI9XXX_DFUNCTR, 2, 0x0A, 0x82,
ILI9XXX_PIXFMT , 1, 0x55,
ILI9XXX_FRMCTR2, 5, 0x0C, 0x0C, 0x00, 0x33, 0x33,
ILI9XXX_ETMOD, 1, 0x35, 0xBB, 1, 0x28,
ILI9XXX_PWCTR1 , 1, 0x0C, // Power control VRH[5:0]
ILI9XXX_PWCTR3 , 2, 0x01, 0xFF,
ILI9XXX_PWCTR4 , 1, 0x10,
ILI9XXX_PWCTR5 , 1, 0x20,
ILI9XXX_IFCTR , 1, 0x0F,
ILI9XXX_PWSET, 2, 0xA4, 0xA1,
ILI9XXX_GMCTRP1 , 14,
0xd0, 0x00, 0x02, 0x07, 0x0a,
0x28, 0x32, 0x44, 0x42, 0x06, 0x0e,
0x12, 0x14, 0x17,
ILI9XXX_GMCTRN1 , 14,
0xd0, 0x00, 0x02, 0x07, 0x0a,
0x28, 0x31, 0x54, 0x47,
0x0e, 0x1c, 0x17, 0x1b,
0x1e,
ILI9XXX_DISPON , 0x80, // Display on
0x00 // End of list
};
// clang-format on
} // namespace ili9xxx
} // namespace esphome

View file

@ -39,7 +39,11 @@ CONF_VCOM_PIN = "vcom_pin"
inkplate6_ns = cg.esphome_ns.namespace("inkplate6")
Inkplate6 = inkplate6_ns.class_(
"Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer
"Inkplate6",
cg.PollingComponent,
i2c.I2CDevice,
display.Display,
display.DisplayBuffer,
)
InkplateModel = inkplate6_ns.enum("InkplateModel")
@ -110,7 +114,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await display.register_display(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -17,7 +17,7 @@ enum InkplateModel : uint8_t {
INKPLATE_6_V2 = 3,
};
class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice {
class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice {
public:
const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95,
0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55};

View file

@ -52,7 +52,6 @@ LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend(
async def setup_lcd_display(var, config):
await cg.register_component(var, config)
await display.register_display(var, config)
cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
if CONF_USER_CHARACTERS in config:

View file

@ -255,12 +255,11 @@ class LD2420Component : public Component, public uart::UARTDevice {
uint16_t gate_energy_[LD2420_TOTAL_GATES];
CmdReplyT cmd_reply_;
uint32_t timeout_;
uint32_t max_distance_gate_;
uint32_t min_distance_gate_;
uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY};
bool cmd_active_{false};
char ld2420_firmware_ver_[8];
char ld2420_firmware_ver_[8]{"v0.0.0"};
bool presence_{false};
bool calibration_{false};
uint16_t distance_{0};

View file

@ -186,25 +186,11 @@ def validate_gpio_usage(value):
return value
BASE_PIN_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ArduinoInternalGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_ANALOG, default=False): cv.boolean,
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
},
)
BASE_PIN_SCHEMA.add_extra(validate_gpio_usage)
BASE_PIN_SCHEMA = pins.gpio_base_schema(
ArduinoInternalGPIOPin,
validate_gpio_pin,
modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
).add_extra(validate_gpio_usage)
async def component_pin_to_code(config):

View file

@ -57,7 +57,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
void start() override { this->initial_run_ = true; }
void apply(AddressableLight &it, const Color &current_color) override {
const uint32_t now = millis();
if (now - this->last_run_ >= this->update_interval_) {
if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) {
this->last_run_ = now;
this->f_(it, current_color, this->initial_run_);
this->initial_run_ = false;

View file

@ -118,7 +118,7 @@ class LambdaLightEffect : public LightEffect {
void start() override { this->initial_run_ = true; }
void apply() override {
const uint32_t now = millis();
if (now - this->last_run_ >= this->update_interval_) {
if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) {
this->last_run_ = now;
this->f_(this->initial_run_);
this->initial_run_ = false;

View file

@ -13,7 +13,6 @@ DEPENDENCIES = ["i2c"]
LilygoT547Touchscreen = lilygo_t5_47_ns.class_(
"LilygoT547Touchscreen",
touchscreen.Touchscreen,
cg.Component,
i2c.I2CDevice,
)
@ -27,17 +26,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
pins.internal_gpio_input_pin_schema
),
}
)
.extend(i2c.i2c_device_schema(0x5A))
.extend(cv.COMPONENT_SCHEMA)
).extend(i2c.i2c_device_schema(0x5A))
)
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)
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
cg.add(var.set_interrupt_pin(interrupt_pin))

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