Merge branch 'dev' into pr/kbx81/7296

This commit is contained in:
Jesse Hills 2024-08-29 13:34:55 +12:00
commit 6606deecbd
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
98 changed files with 2116 additions and 364 deletions

View file

@ -58,6 +58,7 @@ esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1750/* @OttoWinter esphome/components/bh1750/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2 esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
esphome/components/bl0939/* @ziceva esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias- esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas esphome/components/bl0942/* @dbuezas

View file

@ -38,7 +38,7 @@ from esphome.const import (
SECRETS_FILES, SECRETS_FILES,
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import indent, is_ip_address from esphome.helpers import indent, is_ip_address, get_bool_env
from esphome.log import Fore, color, setup_log from esphome.log import Fore, color, setup_log
from esphome.util import ( from esphome.util import (
get_serial_ports, get_serial_ports,
@ -731,7 +731,11 @@ POST_CONFIG_ACTIONS = {
def parse_args(argv): def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False) options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument( options_parser.add_argument(
"-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true" "-v",
"--verbose",
help="Enable verbose ESPHome logs.",
action="store_true",
default=get_bool_env("ESPHOME_VERBOSE"),
) )
options_parser.add_argument( options_parser.add_argument(
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"

View file

@ -1128,6 +1128,19 @@ enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_MUTE = 3; MEDIA_PLAYER_COMMAND_MUTE = 3;
MEDIA_PLAYER_COMMAND_UNMUTE = 4; MEDIA_PLAYER_COMMAND_UNMUTE = 4;
} }
enum MediaPlayerFormatPurpose {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0;
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1;
}
message MediaPlayerSupportedFormat {
option (id) = 119;
option (ifdef) = "USE_MEDIA_PLAYER";
string format = 1;
uint32 sample_rate = 2;
uint32 num_channels = 3;
MediaPlayerFormatPurpose purpose = 4;
}
message ListEntitiesMediaPlayerResponse { message ListEntitiesMediaPlayerResponse {
option (id) = 63; option (id) = 63;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
@ -1143,6 +1156,8 @@ message ListEntitiesMediaPlayerResponse {
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
bool supports_pause = 8; bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
} }
message MediaPlayerStateResponse { message MediaPlayerStateResponse {
option (id) = 64; option (id) = 64;

View file

@ -186,6 +186,7 @@ void APIConnection::loop() {
SubscribeHomeAssistantStateResponse resp; SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id; resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value(); resp.attribute = it.attribute.value();
resp.once = it.once;
if (this->send_subscribe_home_assistant_state_response(resp)) { if (this->send_subscribe_home_assistant_state_response(resp)) {
state_subs_at_++; state_subs_at_++;
} }
@ -1032,6 +1033,15 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
auto traits = media_player->get_traits(); auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause(); msg.supports_pause = traits.get_supports_pause();
for (auto &supported_format : traits.get_supported_formats()) {
MediaPlayerSupportedFormat media_format;
media_format.format = supported_format.format;
media_format.sample_rate = supported_format.sample_rate;
media_format.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
msg.supported_formats.push_back(media_format);
}
return this->send_list_entities_media_player_response(msg); return this->send_list_entities_media_player_response(msg);
} }
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {

View file

@ -387,6 +387,18 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::Me
} }
#endif #endif
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::MediaPlayerFormatPurpose>(enums::MediaPlayerFormatPurpose value) {
switch (value) {
case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT:
return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT";
case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT:
return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT";
default:
return "UNKNOWN";
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<> template<>
const char *proto_enum_to_string<enums::BluetoothDeviceRequestType>(enums::BluetoothDeviceRequestType value) { const char *proto_enum_to_string<enums::BluetoothDeviceRequestType>(enums::BluetoothDeviceRequestType value) {
switch (value) { switch (value) {
@ -5174,6 +5186,64 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #endif
bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->sample_rate = value.as_uint32();
return true;
}
case 3: {
this->num_channels = value.as_uint32();
return true;
}
case 4: {
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
return true;
}
default:
return false;
}
}
bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->format = value.as_string();
return true;
}
default:
return false;
}
}
void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->format);
buffer.encode_uint32(2, this->sample_rate);
buffer.encode_uint32(3, this->num_channels);
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("MediaPlayerSupportedFormat {\n");
out.append(" format: ");
out.append("'").append(this->format).append("'");
out.append("\n");
out.append(" sample_rate: ");
sprintf(buffer, "%" PRIu32, this->sample_rate);
out.append(buffer);
out.append("\n");
out.append(" num_channels: ");
sprintf(buffer, "%" PRIu32, this->num_channels);
out.append(buffer);
out.append("\n");
out.append(" purpose: ");
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
out.append("\n");
out.append("}");
}
#endif
bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {
case 6: { case 6: {
@ -5210,6 +5280,10 @@ bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLeng
this->icon = value.as_string(); this->icon = value.as_string();
return true; return true;
} }
case 9: {
this->supported_formats.push_back(value.as_message<MediaPlayerSupportedFormat>());
return true;
}
default: default:
return false; return false;
} }
@ -5233,6 +5307,9 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default); buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_bool(8, this->supports_pause); buffer.encode_bool(8, this->supports_pause);
for (auto &it : this->supported_formats) {
buffer.encode_message<MediaPlayerSupportedFormat>(9, it, true);
}
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
@ -5270,6 +5347,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
out.append(" supports_pause: "); out.append(" supports_pause: ");
out.append(YESNO(this->supports_pause)); out.append(YESNO(this->supports_pause));
out.append("\n"); out.append("\n");
for (const auto &it : this->supported_formats) {
out.append(" supported_formats: ");
it.dump_to(out);
out.append("\n");
}
out.append("}"); out.append("}");
} }
#endif #endif

View file

@ -156,6 +156,10 @@ enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_MUTE = 3, MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4, MEDIA_PLAYER_COMMAND_UNMUTE = 4,
}; };
enum MediaPlayerFormatPurpose : uint32_t {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0,
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1,
};
enum BluetoothDeviceRequestType : uint32_t { enum BluetoothDeviceRequestType : uint32_t {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0, BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0,
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1, BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1,
@ -1290,6 +1294,21 @@ class ButtonCommandRequest : public ProtoMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
}; };
class MediaPlayerSupportedFormat : public ProtoMessage {
public:
std::string format{};
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesMediaPlayerResponse : public ProtoMessage { class ListEntitiesMediaPlayerResponse : public ProtoMessage {
public: public:
std::string object_id{}; std::string object_id{};
@ -1300,6 +1319,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage {
bool disabled_by_default{false}; bool disabled_by_default{false};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
bool supports_pause{false}; bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View file

@ -321,6 +321,14 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit
#ifdef USE_BUTTON #ifdef USE_BUTTON
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_media_player_supported_format(const MediaPlayerSupportedFormat &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_media_player_supported_format: %s", msg.dump().c_str());
#endif
return this->send_message_<MediaPlayerSupportedFormat>(msg, 119);
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) { bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str());
@ -1149,13 +1157,13 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
case 119: { case 119: {
#ifdef USE_API_NOISE #ifdef USE_MEDIA_PLAYER
NoiseEncryptionSetKeyRequest msg; MediaPlayerSupportedFormat msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_media_player_supported_format: %s", msg.dump().c_str());
#endif #endif
this->on_noise_encryption_set_key_request(msg); this->on_media_player_supported_format(msg);
#endif #endif
break; break;
} }

View file

@ -151,6 +151,10 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_BUTTON #ifdef USE_BUTTON
virtual void on_button_command_request(const ButtonCommandRequest &value){}; virtual void on_button_command_request(const ButtonCommandRequest &value){};
#endif #endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_supported_format(const MediaPlayerSupportedFormat &msg);
virtual void on_media_player_supported_format(const MediaPlayerSupportedFormat &value){};
#endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg); bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg);
#endif #endif

View file

@ -1,34 +1,34 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, spi from esphome.components import sensor, spi
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ID,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
CONF_CURRENT, CONF_CURRENT,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_LINE_FREQUENCY,
CONF_POWER, CONF_POWER,
CONF_POWER_FACTOR, CONF_POWER_FACTOR,
CONF_FREQUENCY, CONF_REACTIVE_POWER,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY, CONF_REVERSE_ACTIVE_ENERGY,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
ICON_LIGHTBULB,
ICON_CURRENT_AC, ICON_CURRENT_AC,
ICON_LIGHTBULB,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING, STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HERTZ, UNIT_HERTZ,
UNIT_VOLT, UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
UNIT_VOLT_AMPS_REACTIVE, UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
CONF_LINE_FREQUENCY = "line_frequency"
CONF_METER_CONSTANT = "meter_constant" CONF_METER_CONSTANT = "meter_constant"
CONF_PL_CONST = "pl_const" CONF_PL_CONST = "pl_const"
CONF_GAIN_PGA = "gain_pga" CONF_GAIN_PGA = "gain_pga"

View file

@ -7,6 +7,7 @@ from esphome.const import (
CONF_FORWARD_ACTIVE_ENERGY, CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY, CONF_FREQUENCY,
CONF_ID, CONF_ID,
CONF_LINE_FREQUENCY,
CONF_PHASE_A, CONF_PHASE_A,
CONF_PHASE_ANGLE, CONF_PHASE_ANGLE,
CONF_PHASE_B, CONF_PHASE_B,
@ -39,7 +40,6 @@ from esphome.const import (
from . import atm90e32_ns from . import atm90e32_ns
CONF_LINE_FREQUENCY = "line_frequency"
CONF_CHIP_TEMPERATURE = "chip_temperature" CONF_CHIP_TEMPERATURE = "chip_temperature"
CONF_GAIN_PGA = "gain_pga" CONF_GAIN_PGA = "gain_pga"
CONF_CURRENT_PHASES = "current_phases" CONF_CURRENT_PHASES = "current_phases"

View file

@ -0,0 +1 @@
CODEOWNERS = ["@athom-tech", "@tarontop", "@jesserockz"]

View file

@ -0,0 +1,238 @@
#include "bl0906.h"
#include "constants.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bl0906 {
static const char *const TAG = "bl0906";
constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; }
// The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated;
constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) {
return (address + data->l + data->m + data->h) ^ 0xFF;
}
void BL0906::loop() {
if (this->current_channel_ == UINT8_MAX) {
return;
}
while (this->available())
this->flush();
if (this->current_channel_ == 0) {
// Temperature
this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_);
} else if (this->current_channel_ == 1) {
this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_);
this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_);
this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_);
} else if (this->current_channel_ == 2) {
this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_);
this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_);
this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_);
} else if (this->current_channel_ == 3) {
this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_);
this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_);
this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_);
} else if (this->current_channel_ == 4) {
this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_);
this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_);
this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_);
} else if (this->current_channel_ == 5) {
this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_);
this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_);
this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_);
} else if (this->current_channel_ == 6) {
this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_);
this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_);
this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 2) {
// Frequency
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_);
// Voltage
this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 1) {
// Total power
this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_);
// Total Energy
this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_);
} else {
this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage
return;
}
this->current_channel_++;
this->handle_actions_();
}
void BL0906::setup() {
while (this->available())
this->flush();
this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE));
// Calibration (1: register address; 2: value before calibration; 3: value after calibration)
this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1
this->bias_correction_(BL0906_RMSOS_2, 0.01500, 0);
this->bias_correction_(BL0906_RMSOS_3, 0.01400, 0);
this->bias_correction_(BL0906_RMSOS_4, 0.01300, 0);
this->bias_correction_(BL0906_RMSOS_5, 0.01200, 0);
this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6
this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD));
}
void BL0906::update() { this->current_channel_ = 0; }
size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) {
this->action_queue_.push_back(function);
return this->action_queue_.size();
}
void BL0906::handle_actions_() {
if (this->action_queue_.empty()) {
return;
}
ActionCallbackFuncPtr ptr_func = nullptr;
for (int i = 0; i < this->action_queue_.size(); i++) {
ptr_func = this->action_queue_[i];
if (ptr_func) {
ESP_LOGI(TAG, "HandleActionCallback[%d]...", i);
(this->*ptr_func)();
}
}
while (this->available()) {
this->read();
}
this->action_queue_.clear();
}
// Reset energy
void BL0906::reset_energy_() {
this->write_array(BL0906_INIT[0], 6);
delay(1);
this->flush();
ESP_LOGW(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_INIT[0][0], BL0906_INIT[0][1], BL0906_INIT[0][2],
BL0906_INIT[0][3], BL0906_INIT[0][4], BL0906_INIT[0][5]);
}
// Read data
void BL0906::read_data_(const uint8_t address, const float reference, sensor::Sensor *sensor) {
if (sensor == nullptr) {
return;
}
DataPacket buffer;
ube24_t data_u24;
sbe24_t data_s24;
float value = 0;
bool signed_result = reference == BL0906_TREF || reference == BL0906_WATT || reference == BL0906_PREF;
this->write_byte(BL0906_READ_COMMAND);
this->write_byte(address);
if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) {
if (bl0906_checksum(address, &buffer) == buffer.checksum) {
if (signed_result) {
data_s24.l = buffer.l;
data_s24.m = buffer.m;
data_s24.h = buffer.h;
} else {
data_u24.l = buffer.l;
data_u24.m = buffer.m;
data_u24.h = buffer.h;
}
} else {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
while (read() >= 0)
;
return;
}
}
// Power
if (reference == BL0906_PREF) {
value = (float) to_int32_t(data_s24) * reference;
}
// Total power
if (reference == BL0906_WATT) {
value = (float) to_int32_t(data_s24) * reference;
}
// Voltage, current, power, total power
if (reference == BL0906_UREF || reference == BL0906_IREF || reference == BL0906_EREF || reference == BL0906_CF) {
value = (float) to_uint32_t(data_u24) * reference;
}
// Frequency
if (reference == BL0906_FREF) {
value = reference / (float) to_uint32_t(data_u24);
}
// Chip temperature
if (reference == BL0906_TREF) {
value = (float) to_int32_t(data_s24);
value = (value - 64) * 12.5 / 59 - 40;
}
sensor->publish_state(value);
}
// RMS offset correction
void BL0906::bias_correction_(uint8_t address, float measurements, float correction) {
DataPacket data;
float ki = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; // Current coefficient
float i_rms0 = measurements * ki;
float i_rms = correction * ki;
int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256;
data.l = value << 24 >> 24;
data.m = value << 16 >> 24;
if (value < 0) {
data.h = (value << 8 >> 24) | 0b10000000;
}
data.address = bl0906_checksum(address, &data);
ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address);
this->write_byte(BL0906_WRITE_COMMAND);
this->write_byte(address);
this->write_byte(data.l);
this->write_byte(data.m);
this->write_byte(data.h);
this->write_byte(data.address);
}
void BL0906::dump_config() {
ESP_LOGCONFIG(TAG, "BL0906:");
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Current1", this->current_1_sensor_);
LOG_SENSOR(" ", "Current2", this->current_2_sensor_);
LOG_SENSOR(" ", "Current3", this->current_3_sensor_);
LOG_SENSOR(" ", "Current4", this->current_4_sensor_);
LOG_SENSOR(" ", "Current5", this->current_5_sensor_);
LOG_SENSOR(" ", "Current6", this->current_6_sensor_);
LOG_SENSOR(" ", "Power1", this->power_1_sensor_);
LOG_SENSOR(" ", "Power2", this->power_2_sensor_);
LOG_SENSOR(" ", "Power3", this->power_3_sensor_);
LOG_SENSOR(" ", "Power4", this->power_4_sensor_);
LOG_SENSOR(" ", "Power5", this->power_5_sensor_);
LOG_SENSOR(" ", "Power6", this->power_6_sensor_);
LOG_SENSOR(" ", "Energy1", this->energy_1_sensor_);
LOG_SENSOR(" ", "Energy2", this->energy_2_sensor_);
LOG_SENSOR(" ", "Energy3", this->energy_3_sensor_);
LOG_SENSOR(" ", "Energy4", this->energy_4_sensor_);
LOG_SENSOR(" ", "Energy5", this->energy_5_sensor_);
LOG_SENSOR(" ", "Energy6", this->energy_6_sensor_);
LOG_SENSOR(" ", "Total Power", this->total_power_sensor_);
LOG_SENSOR(" ", "Total Energy", this->total_energy_sensor_);
LOG_SENSOR(" ", "Frequency", this->frequency_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
}
} // namespace bl0906
} // namespace esphome

View file

@ -0,0 +1,96 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/datatypes.h"
// https://www.belling.com.cn/media/file_object/bel_product/BL0906/datasheet/BL0906_V1.02_cn.pdf
// https://www.belling.com.cn/media/file_object/bel_product/BL0906/guide/BL0906%20APP%20Note_V1.02.pdf
namespace esphome {
namespace bl0906 {
struct DataPacket { // NOLINT(altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
uint8_t h{0};
uint8_t checksum; // checksum
uint8_t address;
} __attribute__((packed));
struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
uint8_t h{0};
} __attribute__((packed));
struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
int8_t h{0};
} __attribute__((packed));
template<typename... Ts> class ResetEnergyAction;
class BL0906;
using ActionCallbackFuncPtr = void (BL0906::*)();
class BL0906 : public PollingComponent, public uart::UARTDevice {
SUB_SENSOR(voltage)
SUB_SENSOR(current_1)
SUB_SENSOR(current_2)
SUB_SENSOR(current_3)
SUB_SENSOR(current_4)
SUB_SENSOR(current_5)
SUB_SENSOR(current_6)
SUB_SENSOR(power_1)
SUB_SENSOR(power_2)
SUB_SENSOR(power_3)
SUB_SENSOR(power_4)
SUB_SENSOR(power_5)
SUB_SENSOR(power_6)
SUB_SENSOR(total_power)
SUB_SENSOR(energy_1)
SUB_SENSOR(energy_2)
SUB_SENSOR(energy_3)
SUB_SENSOR(energy_4)
SUB_SENSOR(energy_5)
SUB_SENSOR(energy_6)
SUB_SENSOR(total_energy)
SUB_SENSOR(frequency)
SUB_SENSOR(temperature)
public:
void loop() override;
void update() override;
void setup() override;
void dump_config() override;
protected:
template<typename... Ts> friend class ResetEnergyAction;
void reset_energy_();
void read_data_(uint8_t address, float reference, sensor::Sensor *sensor);
void bias_correction_(uint8_t address, float measurements, float correction);
uint8_t current_channel_{0};
size_t enqueue_action_(ActionCallbackFuncPtr function);
void handle_actions_();
private:
std::vector<ActionCallbackFuncPtr> action_queue_{};
};
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
public:
void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
};
} // namespace bl0906
} // namespace esphome

View file

@ -0,0 +1,4 @@
# const.py
ICON_ENERGY = "mdi:lightning-bolt"
ICON_FREQUENCY = "mdi:cosine-wave"
ICON_VOLTAGE = "mdi:sine-wave"

View file

@ -0,0 +1,122 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace bl0906 {
// Total power conversion
static const float BL0906_WATT = 16 * 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) /
(40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000);
// Total Energy conversion
static const float BL0906_CF = 16 * 4194304 * 0.032768 * 16 /
(3600000 * 16 *
(40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 /
(1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000))));
// Frequency conversion
static const float BL0906_FREF = 10000000;
// Temperature conversion
static const float BL0906_TREF = 12.5 / 59 - 40;
// Current conversion
static const float BL0906_IREF = 1.097 / (12875 * 1 * (5.1 + 5.1) * 1000 / 2000);
// Voltage conversion
static const float BL0906_UREF = 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (13162 * 1 * 100 * 1000);
// Power conversion
static const float BL0906_PREF = 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) /
(40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000);
// Energy conversion
static const float BL0906_EREF = 4194304 * 0.032768 * 16 /
(3600000 * 16 *
(40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 /
(1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000))));
// Current coefficient
static const float BL0906_KI = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097;
// Power coefficient
static const float BL0906_KP = 40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / 1.097 / 1.097 /
(20000 + 20000 + 20000 + 20000 + 20000);
static const uint8_t USR_WRPROT_WITABLE[6] = {0xCA, 0x9E, 0x55, 0x55, 0x00, 0xB7};
static const uint8_t USR_WRPROT_ONLYREAD[6] = {0xCA, 0x9E, 0x00, 0x00, 0x00, 0x61};
static const uint8_t BL0906_READ_COMMAND = 0x35;
static const uint8_t BL0906_WRITE_COMMAND = 0xCA;
// Register address
// Voltage
static const uint8_t BL0906_V_RMS = 0x16;
// Total power
static const uint8_t BL0906_WATT_SUM = 0X2C;
// Current1~6
static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1
static const uint8_t BL0906_I_2_RMS = 0x0E;
static const uint8_t BL0906_I_3_RMS = 0x0F;
static const uint8_t BL0906_I_4_RMS = 0x10;
static const uint8_t BL0906_I_5_RMS = 0x13;
static const uint8_t BL0906_I_6_RMS = 0x14; // current_6
// Power1~6
static const uint8_t BL0906_WATT_1 = 0X23; // power_1
static const uint8_t BL0906_WATT_2 = 0X24;
static const uint8_t BL0906_WATT_3 = 0X25;
static const uint8_t BL0906_WATT_4 = 0X26;
static const uint8_t BL0906_WATT_5 = 0X29;
static const uint8_t BL0906_WATT_6 = 0X2A; // power_6
// Active pulse count, unsigned
static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1
static const uint8_t BL0906_CF_2_CNT = 0X31;
static const uint8_t BL0906_CF_3_CNT = 0X32;
static const uint8_t BL0906_CF_4_CNT = 0X33;
static const uint8_t BL0906_CF_5_CNT = 0X36;
static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6
// Total active pulse count, unsigned
static const uint8_t BL0906_CF_SUM_CNT = 0X39;
// Voltage frequency cycle
static const uint8_t BL0906_FREQUENCY = 0X4E;
// Internal temperature
static const uint8_t BL0906_TEMPERATURE = 0X5E;
// Calibration register
// RMS gain adjustment register
static const uint8_t BL0906_RMSGN_1 = 0x6D; // Channel_1
static const uint8_t BL0906_RMSGN_2 = 0x6E;
static const uint8_t BL0906_RMSGN_3 = 0x6F;
static const uint8_t BL0906_RMSGN_4 = 0x70;
static const uint8_t BL0906_RMSGN_5 = 0x73;
static const uint8_t BL0906_RMSGN_6 = 0x74; // Channel_6
// RMS offset correction register
static const uint8_t BL0906_RMSOS_1 = 0x78; // Channel_1
static const uint8_t BL0906_RMSOS_2 = 0x79;
static const uint8_t BL0906_RMSOS_3 = 0x7A;
static const uint8_t BL0906_RMSOS_4 = 0x7B;
static const uint8_t BL0906_RMSOS_5 = 0x7E;
static const uint8_t BL0906_RMSOS_6 = 0x7F; // Channel_6
// Active power gain adjustment register
static const uint8_t BL0906_WATTGN_1 = 0xB7; // Channel_1
static const uint8_t BL0906_WATTGN_2 = 0xB8;
static const uint8_t BL0906_WATTGN_3 = 0xB9;
static const uint8_t BL0906_WATTGN_4 = 0xBA;
static const uint8_t BL0906_WATTGN_5 = 0xBD;
static const uint8_t BL0906_WATTGN_6 = 0xBE; // Channel_6
// User write protection setting register,
// You must first write 0x5555 to the write protection setting register before writing to other registers.
static const uint8_t BL0906_USR_WRPROT = 0x9E;
// Reset Register
static const uint8_t BL0906_SOFT_RESET = 0x9F;
const uint8_t BL0906_INIT[2][6] = {
// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
} // namespace bl0906
} // namespace esphome

View file

@ -0,0 +1,184 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_CHANNEL,
CONF_CURRENT,
CONF_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_NAME,
CONF_POWER,
CONF_TEMPERATURE,
CONF_TOTAL_POWER,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
ICON_CURRENT_AC,
ICON_POWER,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_HERTZ,
UNIT_KILOWATT_HOURS,
UNIT_VOLT,
UNIT_WATT,
)
# Import ICONS not included in esphome's const.py, from the local components const.py
from .const import ICON_ENERGY, ICON_FREQUENCY, ICON_VOLTAGE
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["bl0906"]
CONF_TOTAL_ENERGY = "total_energy"
bl0906_ns = cg.esphome_ns.namespace("bl0906")
BL0906 = bl0906_ns.class_("BL0906", cg.PollingComponent, uart.UARTDevice)
ResetEnergyAction = bl0906_ns.class_("ResetEnergyAction", automation.Action)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BL0906),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
icon=ICON_FREQUENCY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_FREQUENCY,
unit_of_measurement=UNIT_HERTZ,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=UNIT_CELSIUS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
icon=ICON_VOLTAGE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
unit_of_measurement=UNIT_VOLT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema(
icon=ICON_POWER,
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER,
unit_of_measurement=UNIT_WATT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TOTAL_ENERGY): sensor.sensor_schema(
icon=ICON_ENERGY,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
unit_of_measurement=UNIT_KILOWATT_HOURS,
),
}
)
.extend(
cv.Schema(
{
cv.Optional(f"{CONF_CHANNEL}_{i + 1}"): cv.Schema(
{
cv.Optional(CONF_CURRENT): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_CURRENT_AC,
accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT,
unit_of_measurement=UNIT_AMPERE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_POWER): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
unit_of_measurement=UNIT_WATT,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_ENERGY): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_ENERGY,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
unit_of_measurement=UNIT_KILOWATT_HOURS,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
}
)
for i in range(6)
}
)
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("60s"))
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"bl0906", baud_rate=19200, require_tx=True, require_rx=True
)
@automation.register_action(
"bl0906.reset_energy",
ResetEnergyAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BL0906),
}
),
)
async def reset_energy_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_frequency_sensor(sens))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
for i in range(6):
if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"):
if current_config := channel_config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(getattr(var, f"set_current_{i + 1}_sensor")(sens))
if power_config := channel_config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(getattr(var, f"set_power_{i + 1}_sensor")(sens))
if energy_config := channel_config.get(CONF_ENERGY):
sens = await sensor.new_sensor(energy_config)
cg.add(getattr(var, f"set_energy_{i + 1}_sensor")(sens))
if total_power_config := config.get(CONF_TOTAL_POWER):
sens = await sensor.new_sensor(total_power_config)
cg.add(var.set_total_power_sensor(sens))
if total_energy_config := config.get(CONF_TOTAL_ENERGY):
sens = await sensor.new_sensor(total_energy_config)
cg.add(var.set_total_energy_sensor(sens))

View file

@ -2,6 +2,8 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes> #include <cinttypes>
// Datasheet: https://www.belling.com.cn/media/file_object/bel_product/BL0942/datasheet/BL0942_V1.06_en.pdf
namespace esphome { namespace esphome {
namespace bl0942 { namespace bl0942 {
@ -12,33 +14,41 @@ static const uint8_t BL0942_FULL_PACKET = 0xAA;
static const uint8_t BL0942_PACKET_HEADER = 0x55; static const uint8_t BL0942_PACKET_HEADER = 0x55;
static const uint8_t BL0942_WRITE_COMMAND = 0xA8; static const uint8_t BL0942_WRITE_COMMAND = 0xA8;
static const uint8_t BL0942_REG_I_FAST_RMS_CTRL = 0x10;
static const uint8_t BL0942_REG_MODE = 0x18; static const uint8_t BL0942_REG_I_RMSOS = 0x12;
static const uint8_t BL0942_REG_SOFT_RESET = 0x19; static const uint8_t BL0942_REG_WA_CREEP = 0x14;
static const uint8_t BL0942_REG_USR_WRPROT = 0x1A; static const uint8_t BL0942_REG_I_FAST_RMS_TH = 0x15;
static const uint8_t BL0942_REG_I_FAST_RMS_CYC = 0x16;
static const uint8_t BL0942_REG_FREQ_CYC = 0x17;
static const uint8_t BL0942_REG_OT_FUNX = 0x18;
static const uint8_t BL0942_REG_MODE = 0x19;
static const uint8_t BL0942_REG_SOFT_RESET = 0x1C;
static const uint8_t BL0942_REG_USR_WRPROT = 0x1D;
static const uint8_t BL0942_REG_TPS_CTRL = 0x1B; static const uint8_t BL0942_REG_TPS_CTRL = 0x1B;
// TODO: Confirm insialisation works as intended static const uint32_t BL0942_REG_MODE_RESV = 0x03;
const uint8_t BL0942_INIT[5][6] = { static const uint32_t BL0942_REG_MODE_CF_EN = 0x04;
// Reset to default static const uint32_t BL0942_REG_MODE_RMS_UPDATE_SEL = 0x08;
{BL0942_WRITE_COMMAND, BL0942_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, static const uint32_t BL0942_REG_MODE_FAST_RMS_SEL = 0x10;
// Enable User Operation Write static const uint32_t BL0942_REG_MODE_AC_FREQ_SEL = 0x20;
{BL0942_WRITE_COMMAND, BL0942_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, static const uint32_t BL0942_REG_MODE_CF_CNT_CLR_SEL = 0x40;
// 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS static const uint32_t BL0942_REG_MODE_CF_CNT_ADD_SEL = 0x80;
{BL0942_WRITE_COMMAND, BL0942_REG_MODE, 0x00, 0x10, 0x00, 0x37}, static const uint32_t BL0942_REG_MODE_UART_RATE_19200 = 0x200;
// 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS static const uint32_t BL0942_REG_MODE_UART_RATE_38400 = 0x300;
{BL0942_WRITE_COMMAND, BL0942_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, static const uint32_t BL0942_REG_MODE_DEFAULT =
// 0x181C = Half cycle, Fast RMS threshold 6172 BL0942_REG_MODE_RESV | BL0942_REG_MODE_CF_EN | BL0942_REG_MODE_CF_CNT_ADD_SEL;
{BL0942_WRITE_COMMAND, BL0942_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}};
static const uint32_t BL0942_REG_SOFT_RESET_MAGIC = 0x5a5a5a;
static const uint32_t BL0942_REG_USR_WRPROT_MAGIC = 0x55;
void BL0942::loop() { void BL0942::loop() {
DataPacket buffer; DataPacket buffer;
if (!this->available()) { if (!this->available()) {
return; return;
} }
if (read_array((uint8_t *) &buffer, sizeof(buffer))) { if (this->read_array((uint8_t *) &buffer, sizeof(buffer))) {
if (validate_checksum(&buffer)) { if (this->validate_checksum_(&buffer)) {
received_package_(&buffer); this->received_package_(&buffer);
} }
} else { } else {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
@ -47,8 +57,8 @@ void BL0942::loop() {
} }
} }
bool BL0942::validate_checksum(DataPacket *data) { bool BL0942::validate_checksum_(DataPacket *data) {
uint8_t checksum = BL0942_READ_COMMAND; uint8_t checksum = BL0942_READ_COMMAND | this->address_;
// Whole package but checksum // Whole package but checksum
uint8_t *raw = (uint8_t *) data; uint8_t *raw = (uint8_t *) data;
for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
@ -61,17 +71,58 @@ bool BL0942::validate_checksum(DataPacket *data) {
return checksum == data->checksum; return checksum == data->checksum;
} }
void BL0942::update() { void BL0942::write_reg_(uint8_t reg, uint32_t val) {
uint8_t pkt[6];
this->flush(); this->flush();
this->write_byte(BL0942_READ_COMMAND); pkt[0] = BL0942_WRITE_COMMAND | this->address_;
pkt[1] = reg;
pkt[2] = (val & 0xff);
pkt[3] = (val >> 8) & 0xff;
pkt[4] = (val >> 16) & 0xff;
pkt[5] = (pkt[0] + pkt[1] + pkt[2] + pkt[3] + pkt[4]) ^ 0xff;
this->write_array(pkt, 6);
delay(1);
}
int BL0942::read_reg_(uint8_t reg) {
union {
uint8_t b[4];
uint32_le_t le32;
} resp;
this->write_byte(BL0942_READ_COMMAND | this->address_);
this->write_byte(reg);
this->flush();
if (this->read_array(resp.b, 4) &&
resp.b[3] ==
(uint8_t) ((BL0942_READ_COMMAND + this->address_ + reg + resp.b[0] + resp.b[1] + resp.b[2]) ^ 0xff)) {
resp.b[3] = 0;
return resp.le32;
}
return -1;
}
void BL0942::update() {
this->write_byte(BL0942_READ_COMMAND | this->address_);
this->write_byte(BL0942_FULL_PACKET); this->write_byte(BL0942_FULL_PACKET);
} }
void BL0942::setup() { void BL0942::setup() {
for (auto *i : BL0942_INIT) { this->write_reg_(BL0942_REG_USR_WRPROT, BL0942_REG_USR_WRPROT_MAGIC);
this->write_array(i, 6); this->write_reg_(BL0942_REG_SOFT_RESET, BL0942_REG_SOFT_RESET_MAGIC);
delay(1);
} uint32_t mode = BL0942_REG_MODE_DEFAULT;
mode |= BL0942_REG_MODE_RMS_UPDATE_SEL; /* 800ms refresh time */
if (this->line_freq_ == LINE_FREQUENCY_60HZ)
mode |= BL0942_REG_MODE_AC_FREQ_SEL;
this->write_reg_(BL0942_REG_MODE, mode);
this->write_reg_(BL0942_REG_USR_WRPROT, 0);
if (this->read_reg_(BL0942_REG_MODE) != mode)
this->status_set_warning("BL0942 setup failed!");
this->flush(); this->flush();
} }
@ -104,13 +155,15 @@ void BL0942::received_package_(DataPacket *data) {
if (frequency_sensor_ != nullptr) { if (frequency_sensor_ != nullptr) {
frequency_sensor_->publish_state(frequency); frequency_sensor_->publish_state(frequency);
} }
this->status_clear_warning();
ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, frequency %fHz, status 0x%08X", v_rms, i_rms, ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, frequency %fHz, status 0x%08X", v_rms, i_rms,
watt, cf_cnt, total_energy_consumption, frequency, data->status); watt, cf_cnt, total_energy_consumption, frequency, data->status);
} }
void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity) void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity)
ESP_LOGCONFIG(TAG, "BL0942:"); ESP_LOGCONFIG(TAG, "BL0942:");
ESP_LOGCONFIG(TAG, " Address: %d", this->address_);
ESP_LOGCONFIG(TAG, " Nominal line frequency: %d Hz", this->line_freq_);
LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Voltage", this->voltage_sensor_);
LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Current", this->current_sensor_);
LOG_SENSOR("", "Power", this->power_sensor_); LOG_SENSOR("", "Power", this->power_sensor_);

View file

@ -28,6 +28,11 @@ struct DataPacket {
uint8_t checksum; uint8_t checksum;
} __attribute__((packed)); } __attribute__((packed));
enum LineFrequency : uint8_t {
LINE_FREQUENCY_50HZ = 50,
LINE_FREQUENCY_60HZ = 60,
};
class BL0942 : public PollingComponent, public uart::UARTDevice { class BL0942 : public PollingComponent, public uart::UARTDevice {
public: public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
@ -35,9 +40,10 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
void set_address(uint8_t address) { this->address_ = address; }
void loop() override; void loop() override;
void update() override; void update() override;
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
@ -59,9 +65,12 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
float current_reference_ = BL0942_IREF; float current_reference_ = BL0942_IREF;
// Divide by this to turn into kWh // Divide by this to turn into kWh
float energy_reference_ = BL0942_EREF; float energy_reference_ = BL0942_EREF;
uint8_t address_ = 0;
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
static bool validate_checksum(DataPacket *data); bool validate_checksum_(DataPacket *data);
int read_reg_(uint8_t reg);
void write_reg_(uint8_t reg, uint32_t val);
void received_package_(DataPacket *data); void received_package_(DataPacket *data);
}; };
} // namespace bl0942 } // namespace bl0942

View file

@ -1,25 +1,27 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ADDRESS,
CONF_CURRENT, CONF_CURRENT,
CONF_ENERGY, CONF_ENERGY,
CONF_FREQUENCY,
CONF_ID, CONF_ID,
CONF_LINE_FREQUENCY,
CONF_POWER, CONF_POWER,
CONF_VOLTAGE, CONF_VOLTAGE,
CONF_FREQUENCY,
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_FREQUENCY,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE, UNIT_AMPERE,
UNIT_HERTZ,
UNIT_KILOWATT_HOURS, UNIT_KILOWATT_HOURS,
UNIT_VOLT, UNIT_VOLT,
UNIT_WATT, UNIT_WATT,
UNIT_HERTZ,
STATE_CLASS_TOTAL_INCREASING,
) )
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@ -27,6 +29,12 @@ DEPENDENCIES = ["uart"]
bl0942_ns = cg.esphome_ns.namespace("bl0942") bl0942_ns = cg.esphome_ns.namespace("bl0942")
BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice) BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice)
LineFrequency = bl0942_ns.enum("LineFrequency")
LINE_FREQS = {
50: LineFrequency.LINE_FREQUENCY_50HZ,
60: LineFrequency.LINE_FREQUENCY_60HZ,
}
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
@ -61,6 +69,14 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_FREQUENCY, device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_LINE_FREQUENCY, default="50HZ"): cv.All(
cv.frequency,
cv.enum(
LINE_FREQS,
int=True,
),
),
cv.Optional(CONF_ADDRESS, default=0): cv.int_range(min=0, max=3),
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
@ -88,3 +104,5 @@ async def to_code(config):
if frequency_config := config.get(CONF_FREQUENCY): if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config) sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_frequency_sensor(sens)) cg.add(var.set_frequency_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_address(config[CONF_ADDRESS]))

View file

@ -186,7 +186,7 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
date_config = config[CONF_DATE] date_config = config[CONF_DATE]
if cg.is_template(date_config): if cg.is_template(date_config):
template_ = await cg.templatable(date_config, [], cg.ESPTime) template_ = await cg.templatable(date_config, args, cg.ESPTime)
cg.add(action_var.set_date(template_)) cg.add(action_var.set_date(template_))
else: else:
date_struct = cg.StructInitializer( date_struct = cg.StructInitializer(
@ -217,7 +217,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
time_config = config[CONF_TIME] time_config = config[CONF_TIME]
if cg.is_template(time_config): if cg.is_template(time_config):
template_ = await cg.templatable(time_config, [], cg.ESPTime) template_ = await cg.templatable(time_config, args, cg.ESPTime)
cg.add(action_var.set_time(template_)) cg.add(action_var.set_time(template_))
else: else:
time_struct = cg.StructInitializer( time_struct = cg.StructInitializer(
@ -248,7 +248,7 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args):
datetime_config = config[CONF_DATETIME] datetime_config = config[CONF_DATETIME]
if cg.is_template(datetime_config): if cg.is_template(datetime_config):
template_ = await cg.templatable(datetime_config, [], cg.ESPTime) template_ = await cg.templatable(datetime_config, args, cg.ESPTime)
cg.add(action_var.set_datetime(template_)) cg.add(action_var.set_datetime(template_))
else: else:
datetime_struct = cg.StructInitializer( datetime_struct = cg.StructInitializer(

View file

@ -172,6 +172,19 @@ def add_idf_component(
KEY_COMPONENTS: components, KEY_COMPONENTS: components,
KEY_SUBMODULES: submodules, KEY_SUBMODULES: submodules,
} }
else:
component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name]
if components is not None:
component_config[KEY_COMPONENTS] = list(
set(component_config[KEY_COMPONENTS] + components)
)
if submodules is not None:
if component_config[KEY_SUBMODULES] is None:
component_config[KEY_SUBMODULES] = submodules
else:
component_config[KEY_SUBMODULES] = list(
set(component_config[KEY_SUBMODULES] + submodules)
)
def add_extra_script(stage: str, filename: str, path: str): def add_extra_script(stage: str, filename: str, path: str):

View file

@ -111,6 +111,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
.flags = flags, .flags = flags,
.identifier = frame->can_id, .identifier = frame->can_id,
.data_length_code = frame->can_data_length_code, .data_length_code = frame->can_data_length_code,
.data = {}, // to suppress warning, data is initialized properly below
}; };
if (!frame->remote_transmission_request) { if (!frame->remote_transmission_request) {
memcpy(message.data, frame->data, frame->can_data_length_code); memcpy(message.data, frame->data, frame->can_data_length_code);

View file

@ -38,7 +38,8 @@ void ESP32RMTLEDStripLightOutput::setup() {
} }
ExternalRAMAllocator<rmt_item32_t> rmt_allocator(ExternalRAMAllocator<rmt_item32_t>::ALLOW_FAILURE); ExternalRAMAllocator<rmt_item32_t> rmt_allocator(ExternalRAMAllocator<rmt_item32_t>::ALLOW_FAILURE);
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 +
1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
rmt_config_t config; rmt_config_t config;
memset(&config, 0, sizeof(config)); memset(&config, 0, sizeof(config));
@ -66,7 +67,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
} }
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low) { uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
// 0-bit // 0-bit
@ -79,6 +80,11 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
this->bit1_.level0 = 1; this->bit1_.level0 = 1;
this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); this->bit1_.duration1 = (uint32_t) (ratio * bit1_low);
this->bit1_.level1 = 0; this->bit1_.level1 = 0;
// reset
this->reset_.duration0 = (uint32_t) (ratio * reset_time_high);
this->reset_.level0 = 1;
this->reset_.duration1 = (uint32_t) (ratio * reset_time_low);
this->reset_.level1 = 0;
} }
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
@ -118,6 +124,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
psrc++; psrc++;
} }
if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) {
pdest->val = this->reset_.val;
pdest++;
len++;
}
if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) {
ESP_LOGE(TAG, "RMT TX error"); ESP_LOGE(TAG, "RMT TX error");
this->status_set_warning(); this->status_set_warning();

View file

@ -49,7 +49,8 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
/// Set a maximum refresh rate in µs as some lights do not like being updated too often. /// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low,
uint32_t reset_time_high, uint32_t reset_time_low);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
@ -75,7 +76,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
bool is_rgbw_; bool is_rgbw_;
bool is_wrgb_; bool is_wrgb_;
rmt_item32_t bit0_, bit1_; rmt_item32_t bit0_, bit1_, reset_;
RGBOrder rgb_order_; RGBOrder rgb_order_;
rmt_channel_t channel_; rmt_channel_t channel_;

View file

@ -43,13 +43,16 @@ class LEDStripTimings:
bit0_low: int bit0_low: int
bit1_high: int bit1_high: int
bit1_low: int bit1_low: int
reset_high: int
reset_low: int
CHIPSETS = { CHIPSETS = {
"WS2812": LEDStripTimings(400, 1000, 1000, 400), "WS2811": LEDStripTimings(300, 1090, 1090, 320, 0, 300000),
"SK6812": LEDStripTimings(300, 900, 600, 600), "WS2812": LEDStripTimings(400, 1000, 1000, 400, 0, 0),
"APA106": LEDStripTimings(350, 1360, 1360, 350), "SK6812": LEDStripTimings(300, 900, 600, 600, 0, 0),
"SM16703": LEDStripTimings(300, 900, 900, 300), "APA106": LEDStripTimings(350, 1360, 1360, 350, 0, 0),
"SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0),
} }
@ -58,6 +61,8 @@ CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low" CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high" CONF_BIT1_HIGH = "bit1_high"
CONF_BIT1_LOW = "bit1_low" CONF_BIT1_LOW = "bit1_low"
CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
@ -88,6 +93,14 @@ CONFIG_SCHEMA = cv.All(
CONF_BIT1_LOW, CONF_BIT1_LOW,
"custom", "custom",
): cv.positive_time_period_nanoseconds, ): cv.positive_time_period_nanoseconds,
cv.Optional(
CONF_RESET_HIGH,
default="0 us",
): cv.positive_time_period_nanoseconds,
cv.Optional(
CONF_RESET_LOW,
default="0 us",
): cv.positive_time_period_nanoseconds,
} }
), ),
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
@ -113,6 +126,8 @@ async def to_code(config):
chipset.bit0_low, chipset.bit0_low,
chipset.bit1_high, chipset.bit1_high,
chipset.bit1_low, chipset.bit1_low,
chipset.reset_high,
chipset.reset_low,
) )
) )
else: else:
@ -122,6 +137,8 @@ async def to_code(config):
config[CONF_BIT0_LOW], config[CONF_BIT0_LOW],
config[CONF_BIT1_HIGH], config[CONF_BIT1_HIGH],
config[CONF_BIT1_LOW], config[CONF_BIT1_LOW],
config[CONF_RESET_HIGH],
config[CONF_RESET_LOW],
) )
) )

View file

@ -1,31 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import core, pins from esphome import core, pins
from esphome.components import display, spi, font import esphome.codegen as cg
from esphome.components import display, font, spi
from esphome.components.display import validate_rotation from esphome.components.display import validate_rotation
from esphome.core import CORE, HexInt import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_COLOR_ORDER,
CONF_COLOR_PALETTE, CONF_COLOR_PALETTE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_ID,
CONF_LAMBDA,
CONF_MODEL,
CONF_RAW_DATA_ID,
CONF_PAGES,
CONF_RESET_PIN,
CONF_DIMENSIONS, CONF_DIMENSIONS,
CONF_WIDTH,
CONF_HEIGHT, CONF_HEIGHT,
CONF_ROTATION, CONF_ID,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X, CONF_MIRROR_X,
CONF_MIRROR_Y, CONF_MIRROR_Y,
CONF_SWAP_XY, CONF_MODEL,
CONF_COLOR_ORDER,
CONF_OFFSET_HEIGHT, CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH, CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_RAW_DATA_ID,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM, CONF_TRANSFORM,
CONF_INVERT_COLORS, CONF_WIDTH,
) )
from esphome.core import CORE, HexInt
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
@ -177,7 +177,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_INVERT_DISPLAY): cv.invalid( cv.Optional(CONF_INVERT_DISPLAY): cv.invalid(
"'invert_display' has been replaced by 'invert_colors'" "'invert_display' has been replaced by 'invert_colors'"
), ),
cv.Optional(CONF_INVERT_COLORS): cv.boolean, cv.Required(CONF_INVERT_COLORS): cv.boolean,
cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True),
cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema(
@ -287,5 +287,4 @@ async def to_code(config):
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_palette(prog_arr)) cg.add(var.set_palette(prog_arr))
if CONF_INVERT_COLORS in config: cg.add(var.invert_colors(config[CONF_INVERT_COLORS]))
cg.add(var.invert_colors(config[CONF_INVERT_COLORS]))

View file

@ -118,6 +118,7 @@ void ILI9XXXDisplay::dump_config() {
ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_)); ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_));
ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_)); ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_));
ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_)); ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_));
ESP_LOGCONFIG(TAG, " Invert colors: %s", YESNO(this->pre_invertcolors_));
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!");
@ -154,7 +155,6 @@ void ILI9XXXDisplay::fill(Color color) {
} }
} }
return; return;
break;
default: default:
new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
break; break;

View file

@ -28,8 +28,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_40MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_40MHZ> {
public: public:
ILI9XXXDisplay() = default; ILI9XXXDisplay() = default;
ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height, bool invert_colors) ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height)
: init_sequence_{init_sequence}, width_{width}, height_{height}, pre_invertcolors_{invert_colors} { : init_sequence_{init_sequence}, width_{width}, height_{height} {
uint8_t cmd, num_args, bits; uint8_t cmd, num_args, bits;
const uint8_t *addr = init_sequence; const uint8_t *addr = init_sequence;
while ((cmd = *addr++) != 0) { while ((cmd = *addr++) != 0) {
@ -144,7 +144,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
bool need_update_ = false; bool need_update_ = false;
bool is_18bitdisplay_ = false; bool is_18bitdisplay_ = false;
PixelMode pixel_mode_{}; PixelMode pixel_mode_{};
bool pre_invertcolors_ = false; bool pre_invertcolors_{};
display::ColorOrder color_order_{display::COLOR_ORDER_BGR}; display::ColorOrder color_order_{display::COLOR_ORDER_BGR};
bool swap_xy_{}; bool swap_xy_{};
bool mirror_x_{}; bool mirror_x_{};
@ -154,54 +154,54 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
//----------- M5Stack display -------------- //----------- M5Stack display --------------
class ILI9XXXM5Stack : public ILI9XXXDisplay { class ILI9XXXM5Stack : public ILI9XXXDisplay {
public: public:
ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240, true) {} ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240) {}
}; };
//----------- M5Stack display -------------- //----------- M5Stack display --------------
class ILI9XXXM5CORE : public ILI9XXXDisplay { class ILI9XXXM5CORE : public ILI9XXXDisplay {
public: public:
ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240, true) {} ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240) {}
}; };
//----------- ST7789V display -------------- //----------- ST7789V display --------------
class ILI9XXXST7789V : public ILI9XXXDisplay { class ILI9XXXST7789V : public ILI9XXXDisplay {
public: public:
ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320, false) {} ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320) {}
}; };
//----------- ILI9XXX_24_TFT display -------------- //----------- ILI9XXX_24_TFT display --------------
class ILI9XXXILI9341 : public ILI9XXXDisplay { class ILI9XXXILI9341 : public ILI9XXXDisplay {
public: public:
ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320, false) {} ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320) {}
}; };
//----------- ILI9XXX_24_TFT rotated display -------------- //----------- ILI9XXX_24_TFT rotated display --------------
class ILI9XXXILI9342 : public ILI9XXXDisplay { class ILI9XXXILI9342 : public ILI9XXXDisplay {
public: public:
ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240, false) {} ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240) {}
}; };
//----------- ILI9XXX_??_TFT rotated display -------------- //----------- ILI9XXX_??_TFT rotated display --------------
class ILI9XXXILI9481 : public ILI9XXXDisplay { class ILI9XXXILI9481 : public ILI9XXXDisplay {
public: public:
ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320, false) {} ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320) {}
}; };
//----------- ILI9481 in 18 bit mode -------------- //----------- ILI9481 in 18 bit mode --------------
class ILI9XXXILI948118 : public ILI9XXXDisplay { class ILI9XXXILI948118 : public ILI9XXXDisplay {
public: public:
ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480, true) {} ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480) {}
}; };
//----------- ILI9XXX_35_TFT rotated display -------------- //----------- ILI9XXX_35_TFT rotated display --------------
class ILI9XXXILI9486 : public ILI9XXXDisplay { class ILI9XXXILI9486 : public ILI9XXXDisplay {
public: public:
ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {} ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320) {}
}; };
class ILI9XXXILI9488 : public ILI9XXXDisplay { class ILI9XXXILI9488 : public ILI9XXXDisplay {
public: public:
ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320, true) {} ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320) {}
protected: protected:
void set_madctl() override { void set_madctl() override {
@ -246,34 +246,34 @@ class WAVESHARERES35 : public ILI9XXXILI9488 {
//----------- ILI9XXX_35_TFT origin colors rotated display -------------- //----------- ILI9XXX_35_TFT origin colors rotated display --------------
class ILI9XXXILI9488A : public ILI9XXXDisplay { class ILI9XXXILI9488A : public ILI9XXXDisplay {
public: public:
ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320, true) {} ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320) {}
}; };
//----------- ILI9XXX_35_TFT rotated display -------------- //----------- ILI9XXX_35_TFT rotated display --------------
class ILI9XXXST7796 : public ILI9XXXDisplay { class ILI9XXXST7796 : public ILI9XXXDisplay {
public: public:
ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480, false) {} ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480) {}
}; };
class ILI9XXXS3Box : public ILI9XXXDisplay { class ILI9XXXS3Box : public ILI9XXXDisplay {
public: public:
ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240, false) {} ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240) {}
}; };
class ILI9XXXS3BoxLite : public ILI9XXXDisplay { class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
public: public:
ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240) {}
}; };
class ILI9XXXGC9A01A : public ILI9XXXDisplay { class ILI9XXXGC9A01A : public ILI9XXXDisplay {
public: public:
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {} ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240) {}
}; };
//----------- ILI9XXX_24_TFT display -------------- //----------- ILI9XXX_24_TFT display --------------
class ILI9XXXST7735 : public ILI9XXXDisplay { class ILI9XXXST7735 : public ILI9XXXDisplay {
public: public:
ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160, false) {} ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160) {}
}; };
} // namespace ili9xxx } // namespace ili9xxx

View file

@ -101,7 +101,6 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = {
ILI9XXX_MADCTL , 1, MADCTL_MV | MADCTL_BGR, // Memory Access Control ILI9XXX_MADCTL , 1, MADCTL_MV | MADCTL_BGR, // Memory Access Control
ILI9XXX_CSCON , 1, 0x01, ILI9XXX_CSCON , 1, 0x01,
ILI9XXX_PIXFMT, 1, 0x55, // 16 bit mode ILI9XXX_PIXFMT, 1, 0x55, // 16 bit mode
ILI9XXX_INVON, 0,
ILI9XXX_DISPON, 0x80, // Set display on ILI9XXX_DISPON, 0x80, // Set display on
0x00 // end 0x00 // end
}; };
@ -121,7 +120,6 @@ static const uint8_t PROGMEM INITCMD_ILI9481_18[] = {
ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control
ILI9XXX_CSCON , 1, 0x01, ILI9XXX_CSCON , 1, 0x01,
ILI9XXX_PIXFMT, 1, 0x66, // 18 bit mode ILI9XXX_PIXFMT, 1, 0x66, // 18 bit mode
ILI9XXX_INVON, 0,
ILI9XXX_DISPON, 0x80, // Set display on ILI9XXX_DISPON, 0x80, // Set display on
0x00 // end 0x00 // end
}; };
@ -204,7 +202,6 @@ static const uint8_t PROGMEM INITCMD_ILI9488_A[] = {
ILI9XXX_SLPOUT, 0x80, // Exit sleep mode ILI9XXX_SLPOUT, 0x80, // Exit sleep mode
//ILI9XXX_INVON , 0,
ILI9XXX_DISPON, 0x80, // Set display on ILI9XXX_DISPON, 0x80, // Set display on
0x00 // end 0x00 // end
}; };

View file

@ -8,6 +8,8 @@
#endif #endif
#include <driver/ledc.h> #include <driver/ledc.h>
#include <cinttypes>
#define CLOCK_FREQUENCY 80e6f #define CLOCK_FREQUENCY 80e6f
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
@ -115,20 +117,22 @@ void LEDCOutput::write_state(float state) {
const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1;
const float duty_rounded = roundf(state * max_duty); const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint32_t>(duty_rounded); auto duty = static_cast<uint32_t>(duty_rounded);
ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_);
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_);
ledcWrite(this->channel_, duty); ledcWrite(this->channel_, duty);
#endif #endif
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
// ensure that 100% on is not 99.975% on
if ((duty == max_duty) && (max_duty != 1)) {
duty = max_duty + 1;
}
auto speed_mode = get_speed_mode(channel_); auto speed_mode = get_speed_mode(channel_);
auto chan_num = static_cast<ledc_channel_t>(channel_ % 8); auto chan_num = static_cast<ledc_channel_t>(channel_ % 8);
int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_);
ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); if (duty == max_duty) {
ledc_update_duty(speed_mode, chan_num); ledc_stop(speed_mode, chan_num, 1);
} else if (duty == 0) {
ledc_stop(speed_mode, chan_num, 0);
} else {
ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint);
ledc_update_duty(speed_mode, chan_num);
}
#endif #endif
} }

View file

@ -21,8 +21,8 @@ from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, update_to_code from .automation import disp_update, focused_widgets, update_to_code
from .defines import CONF_SKIP from .defines import CONF_ADJUSTABLE, CONF_SKIP
from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
from .lv_validation import lv_bool, lv_images_used from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent from .lvcode import LvContext, LvglComponent
@ -67,7 +67,7 @@ from .widgets.lv_bar import bar_spec
from .widgets.meter import meter_spec from .widgets.meter import meter_spec
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
from .widgets.obj import obj_spec from .widgets.obj import obj_spec
from .widgets.page import add_pages, page_spec from .widgets.page import add_pages, generate_page_triggers, page_spec
from .widgets.roller import roller_spec from .widgets.roller import roller_spec
from .widgets.slider import slider_spec from .widgets.slider import slider_spec
from .widgets.spinbox import spinbox_spec from .widgets.spinbox import spinbox_spec
@ -182,6 +182,14 @@ def final_validation(config):
raise cv.Invalid( raise cv.Invalid(
"Using RGBA or RGB24 in image config not compatible with LVGL", path "Using RGBA or RGB24 in image config not compatible with LVGL", path
) )
for w in focused_widgets:
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
if CONF_ADJUSTABLE in widget_conf and not widget_conf[CONF_ADJUSTABLE]:
raise cv.Invalid(
"A non adjustable arc may not be focused",
path,
)
async def to_code(config): async def to_code(config):
@ -266,8 +274,12 @@ async def to_code(config):
await add_top_layer(config) await add_top_layer(config)
await msgboxes_to_code(config) await msgboxes_to_code(config)
await disp_update(f"{lv_component}->get_disp()", config) await disp_update(f"{lv_component}->get_disp()", config)
Widget.set_completed() # At this point only the setup code should be generated
assert LvContext.added_lambda_count == 1
Widget.set_completed()
async with LvContext(lv_component):
await generate_triggers(lv_component) await generate_triggers(lv_component)
await generate_page_triggers(lv_component, config)
for conf in config.get(CONF_ON_IDLE, ()): for conf in config.get(CONF_ON_IDLE, ()):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)

View file

@ -4,12 +4,15 @@ from typing import Callable
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TIMEOUT from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
from esphome.cpp_generator import RawExpression, get_variable
from esphome.cpp_types import nullptr from esphome.cpp_types import nullptr
from .defines import ( from .defines import (
CONF_DISP_BG_COLOR, CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE, CONF_DISP_BG_IMAGE,
CONF_EDITING,
CONF_FREEZE,
CONF_LVGL_ID, CONF_LVGL_ID,
CONF_SHOW_SNOW, CONF_SHOW_SNOW,
literal, literal,
@ -26,8 +29,10 @@ from .lvcode import (
add_line_marks, add_line_marks,
lv, lv,
lv_add, lv_add,
lv_expr,
lv_obj, lv_obj,
lvgl_comp, lvgl_comp,
static_cast,
) )
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
from .types import ( from .types import (
@ -36,9 +41,20 @@ from .types import (
LvglCondition, LvglCondition,
ObjUpdateAction, ObjUpdateAction,
lv_disp_t, lv_disp_t,
lv_group_t,
lv_obj_t, lv_obj_t,
lv_pseudo_button_t,
) )
from .widgets import Widget, get_widgets, lv_scr_act, set_obj_properties from .widgets import (
Widget,
get_widgets,
lv_scr_act,
set_obj_properties,
wait_for_widgets,
)
# Record widgets that are used in a focused action here
focused_widgets = set()
async def action_to_code( async def action_to_code(
@ -48,10 +64,12 @@ async def action_to_code(
template_arg, template_arg,
args, args,
): ):
await wait_for_widgets()
async with LambdaContext(parameters=args, where=action_id) as context: async with LambdaContext(parameters=args, where=action_id) as context:
with LvConditional(lv_expr.is_pre_initialise()):
context.add(RawExpression("return"))
for widget in widgets: for widget in widgets:
with LvConditional(widget.obj != nullptr): await action(widget)
await action(widget)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
return var return var
@ -147,7 +165,7 @@ async def lvgl_update_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config) widgets = await get_widgets(config)
w = widgets[0] w = widgets[0]
disp = f"{w.obj}->get_disp()" disp = f"{w.obj}->get_disp()"
async with LambdaContext(parameters=args, where=action_id) as context: async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
await disp_update(disp, config) await disp_update(disp, config)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, w.var) await cg.register_parented(var, w.var)
@ -224,3 +242,72 @@ async def obj_show_to_code(config, action_id, template_arg, args):
return await action_to_code( return await action_to_code(
await get_widgets(config), do_show, action_id, template_arg, args await get_widgets(config), do_show, action_id, template_arg, args
) )
def focused_id(value):
value = cv.use_id(lv_pseudo_button_t)(value)
focused_widgets.add(value)
return value
@automation.register_action(
"lvgl.widget.focus",
ObjUpdateAction,
cv.Any(
cv.maybe_simple_value(
{
cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
cv.Required(CONF_ACTION): cv.one_of(
"MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True
),
cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent),
cv.Optional(CONF_FREEZE, default=False): cv.boolean,
},
key=CONF_ACTION,
),
cv.maybe_simple_value(
{
cv.Required(CONF_ID): focused_id,
cv.Optional(CONF_FREEZE, default=False): cv.boolean,
cv.Optional(CONF_EDITING, default=False): cv.boolean,
},
key=CONF_ID,
),
),
)
async def widget_focus(config, action_id, template_arg, args):
widget = await get_widgets(config)
if widget:
widget = widget[0]
group = static_cast(
lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj)
)
elif group := config.get(CONF_GROUP):
group = await get_variable(group)
else:
group = lv_expr.group_get_default()
async with LambdaContext(parameters=args, where=action_id) as context:
if widget:
lv.group_focus_freeze(group, False)
lv.group_focus_obj(widget.obj)
if config[CONF_EDITING]:
lv.group_set_editing(group, True)
else:
action = config[CONF_ACTION]
lv_comp = await get_variable(config[CONF_LVGL_ID])
if action == "MARK":
context.add(lv_comp.set_focus_mark(group))
else:
lv.group_focus_freeze(group, False)
if action == "RESTORE":
context.add(lv_comp.restore_focus_mark(group))
elif action == "NEXT":
lv.group_focus_next(group)
else:
lv.group_focus_prev(group)
if config[CONF_FREEZE]:
lv.group_focus_freeze(group, True)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
return var

View file

@ -10,7 +10,7 @@ from ..defines import CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import EVENT_ARG, LambdaContext, LvContext from ..lvcode import EVENT_ARG, LambdaContext, LvContext
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, lv_pseudo_button_t from ..types import LV_EVENT, lv_pseudo_button_t
from ..widgets import Widget, get_widgets from ..widgets import Widget, get_widgets, wait_for_widgets
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
binary_sensor_schema(BinarySensor) binary_sensor_schema(BinarySensor)
@ -29,6 +29,7 @@ async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET) widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0] widget = widget[0]
assert isinstance(widget, Widget) assert isinstance(widget, Widget)
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as pressed_ctx: async with LambdaContext(EVENT_ARG) as pressed_ctx:
pressed_ctx.add(sensor.publish_state(widget.is_pressed())) pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
async with LvContext(paren) as ctx: async with LvContext(paren) as ctx:

View file

@ -6,8 +6,8 @@ Constants already defined in esphome.const are not duplicated here and must be i
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS from esphome.const import CONF_ITEMS
from esphome.core import ID, Lambda from esphome.core import Lambda
from esphome.cpp_generator import MockObj from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32 from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@ -22,19 +22,22 @@ def literal(arg):
return arg return arg
def call_lambda(lamb: LambdaExpression):
expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"):
return expr[7:][:-1]
return f"{lamb}()"
class LValidator: class LValidator:
""" """
A validator for a particular type used in LVGL. Usable in configs as a validator, also A validator for a particular type used in LVGL. Usable in configs as a validator, also
has `process()` to convert a value during code generation has `process()` to convert a value during code generation
""" """
def __init__( def __init__(self, validator, rtype, retmapper=None, requires=None):
self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None
):
self.validator = validator self.validator = validator
self.rtype = rtype self.rtype = rtype
self.idtype = idtype
self.idexpr = idexpr
self.retmapper = retmapper self.retmapper = retmapper
self.requires = requires self.requires = requires
@ -43,8 +46,6 @@ class LValidator:
value = requires_component(self.requires)(value) value = requires_component(self.requires)(value)
if isinstance(value, cv.Lambda): if isinstance(value, cv.Lambda):
return cv.returning_lambda(value) return cv.returning_lambda(value)
if self.idtype is not None and isinstance(value, ID):
return cv.use_id(self.idtype)(value)
return self.validator(value) return self.validator(value)
async def process(self, value, args=()): async def process(self, value, args=()):
@ -52,10 +53,10 @@ class LValidator:
return None return None
if isinstance(value, Lambda): if isinstance(value, Lambda):
return cg.RawExpression( return cg.RawExpression(
f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
) )
if self.idtype is not None and isinstance(value, ID):
return cg.RawExpression(f"{value}->{self.idexpr}")
if self.retmapper is not None: if self.retmapper is not None:
return self.retmapper(value) return self.retmapper(value)
return cg.safe_exp(value) return cg.safe_exp(value)
@ -89,7 +90,7 @@ class LvConstant(LValidator):
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
) )
def mapper(self, value, args=()): def mapper(self, value):
if not isinstance(value, list): if not isinstance(value, list):
value = [value] value = [value]
return literal( return literal(
@ -103,7 +104,7 @@ class LvConstant(LValidator):
def extend(self, *choices): def extend(self, *choices):
""" """
Extend an LVCconstant with additional choices. Extend an LVconstant with additional choices.
:param choices: The extra choices :param choices: The extra choices
:return: A new LVConstant instance :return: A new LVConstant instance
""" """
@ -147,6 +148,7 @@ LV_EVENT_MAP = {
"DEFOCUS": "DEFOCUSED", "DEFOCUS": "DEFOCUSED",
"READY": "READY", "READY": "READY",
"CANCEL": "CANCEL", "CANCEL": "CANCEL",
"ALL_EVENTS": "ALL",
} }
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
@ -389,6 +391,7 @@ CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_GROUP = "default_group" CONF_DEFAULT_GROUP = "default_group"
CONF_DIR = "dir" CONF_DIR = "dir"
CONF_DISPLAYS = "displays" CONF_DISPLAYS = "displays"
CONF_EDITING = "editing"
CONF_ENCODERS = "encoders" CONF_ENCODERS = "encoders"
CONF_END_ANGLE = "end_angle" CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value" CONF_END_VALUE = "end_value"
@ -400,6 +403,7 @@ CONF_FLEX_ALIGN_MAIN = "flex_align_main"
CONF_FLEX_ALIGN_CROSS = "flex_align_cross" CONF_FLEX_ALIGN_CROSS = "flex_align_cross"
CONF_FLEX_ALIGN_TRACK = "flex_align_track" CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow" CONF_FLEX_GROW = "flex_grow"
CONF_FREEZE = "freeze"
CONF_FULL_REFRESH = "full_refresh" CONF_FULL_REFRESH = "full_refresh"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos" CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
@ -427,10 +431,12 @@ CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj" CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x" CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y" CONF_OFFSET_Y = "offset_y"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line" CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select" CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next" CONF_NEXT = "next"
CONF_PAD_ROW = "pad_row"
CONF_PAD_COLUMN = "pad_column"
CONF_PAGE = "page" CONF_PAGE = "page"
CONF_PAGE_WRAP = "page_wrap" CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode" CONF_PASSWORD_MODE = "password_mode"
@ -462,6 +468,7 @@ CONF_SKIP = "skip"
CONF_SYMBOL = "symbol" CONF_SYMBOL = "symbol"
CONF_TAB_ID = "tab_id" CONF_TAB_ID = "tab_id"
CONF_TABS = "tabs" CONF_TABS = "tabs"
CONF_TIME_FORMAT = "time_format"
CONF_TILE = "tile" CONF_TILE = "tile"
CONF_TILE_ID = "tile_id" CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles" CONF_TILES = "tiles"
@ -501,4 +508,10 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""): def join_enums(enums, prefix=""):
return literal("|".join(f"(int){prefix}{e.upper()}" for e in enums)) enums = list(enums)
enums.sort()
# If a prefix is provided, prepend each constant with the prefix, and assume that all the constants are within the
# same namespace, otherwise cast to int to avoid triggering warnings about mixing enum types.
if prefix:
return literal("|".join(f"{prefix}{e.upper()}" for e in enums))
return literal("|".join(f"(int){e.upper()}" for e in enums))

View file

@ -8,7 +8,7 @@ from ..defines import CONF_LVGL_ID
from ..lvcode import LvContext from ..lvcode import LvContext
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LvType, lvgl_ns from ..types import LvType, lvgl_ns
from ..widgets import get_widgets from ..widgets import get_widgets, wait_for_widgets
lv_led_t = LvType("lv_led_t") lv_led_t = LvType("lv_led_t")
LVLight = lvgl_ns.class_("LVLight", LightOutput) LVLight = lvgl_ns.class_("LVLight", LightOutput)
@ -28,5 +28,6 @@ async def to_code(config):
paren = await cg.get_variable(config[CONF_LVGL_ID]) paren = await cg.get_variable(config[CONF_LVGL_ID])
widget = await get_widgets(config, CONF_LED) widget = await get_widgets(config, CONF_LED)
widget = widget[0] widget = widget[0]
await wait_for_widgets()
async with LvContext(paren) as ctx: async with LvContext(paren) as ctx:
ctx.add(var.set_obj(widget.obj)) ctx.add(var.set_obj(widget.obj))

View file

@ -1,17 +1,14 @@
from typing import Union from typing import Union
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct from esphome.components.color import ColorStruct
from esphome.components.font import Font from esphome.components.font import Font
from esphome.components.image import Image_ from esphome.components.image import Image_
from esphome.components.sensor import Sensor
from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE
from esphome.core import HexInt from esphome.core import HexInt, Lambda
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.cpp_types import uint32 from esphome.cpp_types import ESPTime, uint32
from esphome.helpers import cpp_string_escape from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@ -19,9 +16,11 @@ from . import types as ty
from .defines import ( from .defines import (
CONF_END_VALUE, CONF_END_VALUE,
CONF_START_VALUE, CONF_START_VALUE,
CONF_TIME_FORMAT,
LV_FONTS, LV_FONTS,
LValidator, LValidator,
LvConstant, LvConstant,
call_lambda,
literal, literal,
) )
from .helpers import ( from .helpers import (
@ -110,13 +109,13 @@ def angle(value):
def size_validator(value): def size_validator(value):
"""A size in one axis - one of "size_content", a number (pixels) or a percentage""" """A size in one axis - one of "size_content", a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return ["size_content", "pixels", "..%"] return ["SIZE_CONTENT", "number of pixels", "percentage"]
if isinstance(value, str) and value.lower().endswith("px"): if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2]) value = cv.int_(value[:-2])
if isinstance(value, str) and not value.endswith("%"): if isinstance(value, str) and not value.endswith("%"):
if value.upper() == "SIZE_CONTENT": if value.upper() == "SIZE_CONTENT":
return "LV_SIZE_CONTENT" return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a pixel position or a percentage") raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)")
if isinstance(value, int): if isinstance(value, int):
return cv.int_(value) return cv.int_(value)
# Will throw an exception if not a percentage. # Will throw an exception if not a percentage.
@ -125,6 +124,15 @@ def size_validator(value):
size = LValidator(size_validator, uint32, retmapper=literal) size = LValidator(size_validator, uint32, retmapper=literal)
def pixels_validator(value):
if isinstance(value, str) and value.lower().endswith("px"):
return cv.int_(value[:-2])
return cv.int_(value)
pixels = LValidator(pixels_validator, uint32, retmapper=literal)
radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
@ -167,9 +175,7 @@ lv_image = LValidator(
retmapper=lambda x: lv_expr.img_from(MockObj(x)), retmapper=lambda x: lv_expr.img_from(MockObj(x)),
requires="image", requires="image",
) )
lv_bool = LValidator( lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal
)
def lv_pct(value: Union[int, float]): def lv_pct(value: Union[int, float]):
@ -185,42 +191,60 @@ def lvms_validator_(value):
lv_milliseconds = LValidator( lv_milliseconds = LValidator(
lvms_validator_, lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds
cg.int32,
retmapper=lambda x: x.total_milliseconds,
) )
class TextValidator(LValidator): class TextValidator(LValidator):
def __init__(self): def __init__(self):
super().__init__( super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}"))
cv.string,
cg.const_char_ptr,
TextSensor,
"get_state().c_str()",
lambda s: cg.safe_exp(f"{s}"),
)
def __call__(self, value): def __call__(self, value):
if isinstance(value, dict): if isinstance(value, dict) and CONF_FORMAT in value:
return value return value
return super().__call__(value) return super().__call__(value)
async def process(self, value, args=()): async def process(self, value, args=()):
if isinstance(value, dict): if isinstance(value, dict):
args = [str(x) for x in value[CONF_ARGS]] if format_str := value.get(CONF_FORMAT):
arg_expr = cg.RawExpression(",".join(args)) args = [str(x) for x in value[CONF_ARGS]]
format_str = cpp_string_escape(value[CONF_FORMAT]) arg_expr = cg.RawExpression(",".join(args))
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") format_str = cpp_string_escape(format_str)
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
if time_format := value.get(CONF_TIME_FORMAT):
source = value[CONF_TIME]
if isinstance(source, Lambda):
time_format = cpp_string_escape(time_format)
return cg.RawExpression(
call_lambda(
await cg.process_lambda(source, args, return_type=ESPTime)
)
+ f".strftime({time_format}).c_str()"
)
# must be an ID
source = await cg.get_variable(source)
return source.now().strftime(time_format).c_str()
if isinstance(value, Lambda):
value = call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
# Was the lambda call reduced to a string?
if value.endswith("c_str()") or (
value.endswith('"') and value.startswith('"')
):
pass
else:
# Either a std::string or a lambda call returning that. We need const char*
value = f"({value}).c_str()"
return cg.RawExpression(value)
return await super().process(value, args) return await super().process(value, args)
lv_text = TextValidator() lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") lv_float = LValidator(cv.float_, cg.float_)
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") lv_int = LValidator(cv.int_, cg.int_)
lv_brightness = LValidator( lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255)
)
def is_lv_font(font): def is_lv_font(font):

View file

@ -28,7 +28,7 @@ LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp()
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)]
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
EVENT_ARG = [(lv_event_t_ptr, "ev")] EVENT_ARG = [(lv_event_t_ptr, "event")]
# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction; # Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction;
# UPDATE_EVENT is fired when an entity is programmatically updated locally. # UPDATE_EVENT is fired when an entity is programmatically updated locally.
# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction. # VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction.
@ -176,6 +176,8 @@ class LvContext(LambdaContext):
Code generation into the LVGL initialisation code (called in `setup()`) Code generation into the LVGL initialisation code (called in `setup()`)
""" """
added_lambda_count = 0
def __init__(self, lv_component, args=None): def __init__(self, lv_component, args=None):
self.args = args or LVGL_COMP_ARG self.args = args or LVGL_COMP_ARG
super().__init__(parameters=self.args) super().__init__(parameters=self.args)
@ -183,6 +185,7 @@ class LvContext(LambdaContext):
async def add_init_lambda(self): async def add_init_lambda(self):
cg.add(self.lv_component.add_init_lambda(await self.get_lambda())) cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
LvContext.added_lambda_count += 1
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb) await super().__aexit__(exc_type, exc_val, exc_tb)
@ -288,6 +291,10 @@ class LvExpr(MockLv):
pass pass
def static_cast(type, value):
return literal(f"static_cast<{type}>({value})")
# Top level mock for generic lv_ calls to be recorded # Top level mock for generic lv_ calls to be recorded
lv = MockLv("lv_") lv = MockLv("lv_")
# Just generate an expression # Just generate an expression

View file

@ -15,6 +15,60 @@ static void log_cb(const char *buf) {
} }
#endif // LV_USE_LOG #endif // LV_USE_LOG
static const char *const EVENT_NAMES[] = {
"NONE",
"PRESSED",
"PRESSING",
"PRESS_LOST",
"SHORT_CLICKED",
"LONG_PRESSED",
"LONG_PRESSED_REPEAT",
"CLICKED",
"RELEASED",
"SCROLL_BEGIN",
"SCROLL_END",
"SCROLL",
"GESTURE",
"KEY",
"FOCUSED",
"DEFOCUSED",
"LEAVE",
"HIT_TEST",
"COVER_CHECK",
"REFR_EXT_DRAW_SIZE",
"DRAW_MAIN_BEGIN",
"DRAW_MAIN",
"DRAW_MAIN_END",
"DRAW_POST_BEGIN",
"DRAW_POST",
"DRAW_POST_END",
"DRAW_PART_BEGIN",
"DRAW_PART_END",
"VALUE_CHANGED",
"INSERT",
"REFRESH",
"READY",
"CANCEL",
"DELETE",
"CHILD_CHANGED",
"CHILD_CREATED",
"CHILD_DELETED",
"SCREEN_UNLOAD_START",
"SCREEN_LOAD_START",
"SCREEN_LOADED",
"SCREEN_UNLOADED",
"SIZE_CHANGED",
"STYLE_CHANGED",
"LAYOUT_CHANGED",
"GET_SELF_SIZE",
};
std::string lv_event_code_name_for(uint8_t event_code) {
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
return EVENT_NAMES[event_code];
}
return str_sprintf("%2d", event_code);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
// make sure all coordinates are even // make sure all coordinates are even
if (area->x1 & 1) if (area->x1 & 1)
@ -294,6 +348,13 @@ void LvglComponent::loop() {
} }
lv_timer_handler_run_in_period(5); lv_timer_handler_run_in_period(5);
} }
bool lv_is_pre_initialise() {
if (!lv_is_initialized()) {
ESP_LOGE(TAG, "LVGL call before component is initialised");
return true;
}
return false;
}
#ifdef USE_LVGL_IMAGE #ifdef USE_LVGL_IMAGE
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) {

View file

@ -40,6 +40,8 @@ namespace lvgl {
extern lv_event_code_t lv_api_event; // NOLINT extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT extern lv_event_code_t lv_update_event; // NOLINT
extern std::string lv_event_code_name_for(uint8_t event_code);
extern bool lv_is_pre_initialise();
#ifdef USE_LVGL_COLOR #ifdef USE_LVGL_COLOR
inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif // USE_LVGL_COLOR #endif // USE_LVGL_COLOR
@ -142,6 +144,13 @@ class LvglComponent : public PollingComponent {
void show_next_page(lv_scr_load_anim_t anim, uint32_t time); void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
void restore_focus_mark(lv_group_t *group) {
auto *mark = this->focus_marks_[group];
if (mark != nullptr) {
lv_group_focus_obj(mark);
}
}
protected: protected:
void write_random_(); void write_random_();
@ -157,6 +166,7 @@ class LvglComponent : public PollingComponent {
bool show_snow_{}; bool show_snow_{};
lv_coord_t snow_line_{}; lv_coord_t snow_line_{};
bool page_wrap_{true}; bool page_wrap_{true};
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_; std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{}; CallbackManager<void(uint32_t)> idle_callbacks_{};

View file

@ -16,7 +16,7 @@ from ..lvcode import (
) )
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, LvNumber, lvgl_ns from ..types import LV_EVENT, LvNumber, lvgl_ns
from ..widgets import get_widgets from ..widgets import get_widgets, wait_for_widgets
LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number)
@ -44,11 +44,13 @@ async def to_code(config):
step=widget.get_step(), step=widget.get_step(),
) )
await wait_for_widgets()
async with LambdaContext([(cg.float_, "v")]) as control: async with LambdaContext([(cg.float_, "v")]) as control:
await widget.set_property( await widget.set_property(
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
) )
lv.event_send(widget.obj, API_EVENT, cg.nullptr) lv.event_send(widget.obj, API_EVENT, cg.nullptr)
control.add(var.publish_state(widget.get_value()))
async with LambdaContext(EVENT_ARG) as event: async with LambdaContext(EVENT_ARG) as event:
event.add(var.publish_state(widget.get_value())) event.add(var.publish_state(widget.get_value()))
event_code = ( event_code = (

View file

@ -1,5 +1,6 @@
from esphome import config_validation as cv from esphome import config_validation as cv
from esphome.automation import Trigger, validate_automation from esphome.automation import Trigger, validate_automation
from esphome.components.time import RealTimeClock
from esphome.const import ( from esphome.const import (
CONF_ARGS, CONF_ARGS,
CONF_FORMAT, CONF_FORMAT,
@ -8,6 +9,7 @@ from esphome.const import (
CONF_ON_VALUE, CONF_ON_VALUE,
CONF_STATE, CONF_STATE,
CONF_TEXT, CONF_TEXT,
CONF_TIME,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
) )
@ -15,9 +17,10 @@ from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT
from .helpers import add_lv_use, requires_component, validate_printf from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_image from .lv_validation import lv_color, lv_font, lv_image
from .lvcode import LvglComponent from .lvcode import LvglComponent, lv_event_t_ptr
from .types import ( from .types import (
LVEncoderListener, LVEncoderListener,
LvType, LvType,
@ -46,7 +49,13 @@ TEXT_SCHEMA = cv.Schema(
), ),
validate_printf, validate_printf,
), ),
lvalid.lv_text, cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
),
cv.templatable(cv.string),
) )
} }
) )
@ -116,15 +125,13 @@ STYLE_PROPS = {
"opa_layered": lvalid.opacity, "opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color, "outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity, "outline_opa": lvalid.opacity,
"outline_pad": lvalid.size, "outline_pad": lvalid.pixels,
"outline_width": lvalid.size, "outline_width": lvalid.pixels,
"pad_all": lvalid.size, "pad_all": lvalid.pixels,
"pad_bottom": lvalid.size, "pad_bottom": lvalid.pixels,
"pad_column": lvalid.size, "pad_left": lvalid.pixels,
"pad_left": lvalid.size, "pad_right": lvalid.pixels,
"pad_right": lvalid.size, "pad_top": lvalid.pixels,
"pad_row": lvalid.size,
"pad_top": lvalid.size,
"shadow_color": lvalid.lv_color, "shadow_color": lvalid.lv_color,
"shadow_ofs_x": cv.int_, "shadow_ofs_x": cv.int_,
"shadow_ofs_y": cv.int_, "shadow_ofs_y": cv.int_,
@ -208,14 +215,12 @@ def automation_schema(typ: LvType):
events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,)
else: else:
events = df.LV_EVENT_TRIGGERS events = df.LV_EVENT_TRIGGERS
if isinstance(typ, LvType): args = [typ.get_arg_type()] if isinstance(typ, LvType) else []
template = Trigger.template(typ.get_arg_type()) args.append(lv_event_t_ptr)
else:
template = Trigger.template()
return { return {
cv.Optional(event): validate_automation( cv.Optional(event): validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(template), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template(*args)),
} }
) )
for event in events for event in events
@ -304,6 +309,8 @@ LAYOUT_SCHEMA = {
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
}, },
df.TYPE_FLEX: { df.TYPE_FLEX: {
cv.Optional( cv.Optional(
@ -312,6 +319,8 @@ LAYOUT_SCHEMA = {
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
}, },
}, },
lower=True, lower=True,
@ -338,7 +347,6 @@ DISP_BG_SCHEMA = cv.Schema(
} }
) )
# A style schema that can include text # A style schema that can include text
STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT

View file

@ -15,7 +15,7 @@ from ..lvcode import (
) )
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, LvSelect, lvgl_ns from ..types import LV_EVENT, LvSelect, lvgl_ns
from ..widgets import get_widgets from ..widgets import get_widgets, wait_for_widgets
LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select)
@ -37,11 +37,13 @@ async def to_code(config):
options = widget.config.get(CONF_OPTIONS, []) options = widget.config.get(CONF_OPTIONS, [])
selector = await select.new_select(config, options=options) selector = await select.new_select(config, options=options)
paren = await cg.get_variable(config[CONF_LVGL_ID]) paren = await cg.get_variable(config[CONF_LVGL_ID])
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as pub_ctx: async with LambdaContext(EVENT_ARG) as pub_ctx:
pub_ctx.add(selector.publish_index(widget.get_value())) pub_ctx.add(selector.publish_index(widget.get_value()))
async with LambdaContext([(cg.uint16, "v")]) as control: async with LambdaContext([(cg.uint16, "v")]) as control:
await widget.set_property("selected", "v", animated=config[CONF_ANIMATED]) await widget.set_property("selected", "v", animated=config[CONF_ANIMATED])
lv.event_send(widget.obj, API_EVENT, cg.nullptr) lv.event_send(widget.obj, API_EVENT, cg.nullptr)
control.add(selector.publish_index(widget.get_value()))
async with LvContext(paren) as ctx: async with LvContext(paren) as ctx:
lv_add(selector.set_control_lambda(await control.get_lambda())) lv_add(selector.set_control_lambda(await control.get_lambda()))
ctx.add( ctx.add(

View file

@ -14,7 +14,7 @@ from ..lvcode import (
) )
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, LvNumber from ..types import LV_EVENT, LvNumber
from ..widgets import Widget, get_widgets from ..widgets import Widget, get_widgets, wait_for_widgets
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
sensor_schema(Sensor) sensor_schema(Sensor)
@ -33,6 +33,7 @@ async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET) widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0] widget = widget[0]
assert isinstance(widget, Widget) assert isinstance(widget, Widget)
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as lamb: async with LambdaContext(EVENT_ARG) as lamb:
lv_add(sensor.publish_state(widget.get_value())) lv_add(sensor.publish_state(widget.get_value()))
async with LvContext(paren, LVGL_COMP_ARG): async with LvContext(paren, LVGL_COMP_ARG):

View file

@ -3,7 +3,7 @@ from esphome.components.switch import Switch, new_switch, switch_schema
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from ..defines import CONF_LVGL_ID, CONF_WIDGET from ..defines import CONF_LVGL_ID, CONF_WIDGET, literal
from ..lvcode import ( from ..lvcode import (
API_EVENT, API_EVENT,
EVENT_ARG, EVENT_ARG,
@ -16,7 +16,7 @@ from ..lvcode import (
) )
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
from ..widgets import get_widgets from ..widgets import get_widgets, wait_for_widgets
LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch)
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
@ -35,6 +35,7 @@ async def to_code(config):
paren = await cg.get_variable(config[CONF_LVGL_ID]) paren = await cg.get_variable(config[CONF_LVGL_ID])
widget = await get_widgets(config, CONF_WIDGET) widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0] widget = widget[0]
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as checked_ctx: async with LambdaContext(EVENT_ARG) as checked_ctx:
checked_ctx.add(switch.publish_state(widget.get_value())) checked_ctx.add(switch.publish_state(widget.get_value()))
async with LambdaContext([(cg.bool_, "v")]) as control: async with LambdaContext([(cg.bool_, "v")]) as control:
@ -43,6 +44,7 @@ async def to_code(config):
cond.else_() cond.else_()
widget.clear_state(LV_STATE.CHECKED) widget.clear_state(LV_STATE.CHECKED)
lv.event_send(widget.obj, API_EVENT, cg.nullptr) lv.event_send(widget.obj, API_EVENT, cg.nullptr)
control.add(switch.publish_state(literal("v")))
async with LvContext(paren) as ctx: async with LvContext(paren) as ctx:
lv_add(switch.set_control_lambda(await control.get_lambda())) lv_add(switch.set_control_lambda(await control.get_lambda()))
ctx.add( ctx.add(

View file

@ -15,7 +15,7 @@ from ..lvcode import (
) )
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, LvText, lvgl_ns from ..types import LV_EVENT, LvText, lvgl_ns
from ..widgets import get_widgets from ..widgets import get_widgets, wait_for_widgets
LVGLText = lvgl_ns.class_("LVGLText", text.Text) LVGLText = lvgl_ns.class_("LVGLText", text.Text)
@ -32,9 +32,11 @@ async def to_code(config):
paren = await cg.get_variable(config[CONF_LVGL_ID]) paren = await cg.get_variable(config[CONF_LVGL_ID])
widget = await get_widgets(config, CONF_WIDGET) widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0] widget = widget[0]
await wait_for_widgets()
async with LambdaContext([(cg.std_string, "text_value")]) as control: async with LambdaContext([(cg.std_string, "text_value")]) as control:
await widget.set_property("text", "text_value.c_str())") await widget.set_property("text", "text_value.c_str())")
lv.event_send(widget.obj, API_EVENT, None) lv.event_send(widget.obj, API_EVENT, None)
control.add(textvar.publish_state(widget.get_value()))
async with LambdaContext(EVENT_ARG) as lamb: async with LambdaContext(EVENT_ARG) as lamb:
lv_add(textvar.publish_state(widget.get_value())) lv_add(textvar.publish_state(widget.get_value()))
async with LvContext(paren): async with LvContext(paren):

View file

@ -10,7 +10,7 @@ from ..defines import CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LV_EVENT, LvText from ..types import LV_EVENT, LvText
from ..widgets import get_widgets from ..widgets import get_widgets, wait_for_widgets
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
text_sensor_schema(TextSensor) text_sensor_schema(TextSensor)
@ -28,6 +28,7 @@ async def to_code(config):
paren = await cg.get_variable(config[CONF_LVGL_ID]) paren = await cg.get_variable(config[CONF_LVGL_ID])
widget = await get_widgets(config, CONF_WIDGET) widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0] widget = widget[0]
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as pressed_ctx: async with LambdaContext(EVENT_ARG) as pressed_ctx:
pressed_ctx.add(sensor.publish_state(widget.get_value())) pressed_ctx.add(sensor.publish_state(widget.get_value()))
async with LvContext(paren) as ctx: async with LvContext(paren) as ctx:

View file

@ -19,6 +19,7 @@ from .lvcode import (
LvConditional, LvConditional,
lv, lv,
lv_add, lv_add,
lv_event_t_ptr,
) )
from .types import LV_EVENT from .types import LV_EVENT
from .widgets import widget_map from .widgets import widget_map
@ -65,10 +66,10 @@ async def generate_triggers(lv_component):
async def add_trigger(conf, lv_component, w, *events): async def add_trigger(conf, lv_component, w, *events):
tid = conf[CONF_TRIGGER_ID] tid = conf[CONF_TRIGGER_ID]
trigger = cg.new_Pvariable(tid) trigger = cg.new_Pvariable(tid)
args = w.get_args() args = w.get_args() + [(lv_event_t_ptr, "event")]
value = w.get_value() value = w.get_value()
await automation.build_automation(trigger, args, conf) await automation.build_automation(trigger, args, conf)
async with LambdaContext(EVENT_ARG, where=tid) as context: async with LambdaContext(EVENT_ARG, where=tid) as context:
with LvConditional(w.is_selected()): with LvConditional(w.is_selected()):
lv_add(trigger.trigger(value)) lv_add(trigger.trigger(value, literal("event")))
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events)) lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events))

View file

@ -57,7 +57,7 @@ lv_group_t = cg.global_ns.struct("lv_group_t")
LVTouchListener = lvgl_ns.class_("LVTouchListener") LVTouchListener = lvgl_ns.class_("LVTouchListener")
LVEncoderListener = lvgl_ns.class_("LVEncoderListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t") lv_obj_t = LvType("lv_obj_t")
lv_page_t = cg.global_ns.class_("LvPageType", LvCompound) lv_page_t = LvType("LvPageType", parents=(LvCompound,))
lv_img_t = LvType("lv_img_t") lv_img_t = LvType("lv_img_t")
LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_EVENT = MockObj(base="LV_EVENT_", op="")

View file

@ -20,6 +20,8 @@ from ..defines import (
CONF_GRID_ROWS, CONF_GRID_ROWS,
CONF_LAYOUT, CONF_LAYOUT,
CONF_MAIN, CONF_MAIN,
CONF_PAD_COLUMN,
CONF_PAD_ROW,
CONF_SCROLLBAR_MODE, CONF_SCROLLBAR_MODE,
CONF_STYLES, CONF_STYLES,
CONF_WIDGETS, CONF_WIDGETS,
@ -29,6 +31,7 @@ from ..defines import (
TYPE_FLEX, TYPE_FLEX,
TYPE_GRID, TYPE_GRID,
LValidator, LValidator,
call_lambda,
join_enums, join_enums,
literal, literal,
) )
@ -115,7 +118,14 @@ class Widget:
def clear_flag(self, flag): def clear_flag(self, flag):
return lv_obj.clear_flag(self.obj, literal(flag)) return lv_obj.clear_flag(self.obj, literal(flag))
async def set_property(self, prop, value, animated: bool = None): async def set_property(self, prop, value, animated: bool = None, lv_name=None):
"""
Set a property of the widget.
:param prop: The property name
:param value: The value
:param animated: If the change should be animated
:param lv_name: The base type of the widget e.g. "obj"
"""
if isinstance(value, dict): if isinstance(value, dict):
value = value.get(prop) value = value.get(prop)
if isinstance(ALL_STYLES.get(prop), LValidator): if isinstance(ALL_STYLES.get(prop), LValidator):
@ -128,11 +138,12 @@ class Widget:
value = value.total_milliseconds value = value.total_milliseconds
if isinstance(value, str): if isinstance(value, str):
value = literal(value) value = literal(value)
lv_name = lv_name or self.type.lv_name
if animated is None or self.type.animated is not True: if animated is None or self.type.animated is not True:
lv.call(f"{self.type.lv_name}_set_{prop}", self.obj, value) lv.call(f"{lv_name}_set_{prop}", self.obj, value)
else: else:
lv.call( lv.call(
f"{self.type.lv_name}_set_{prop}", f"{lv_name}_set_{prop}",
self.obj, self.obj,
value, value,
literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"), literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"),
@ -214,12 +225,25 @@ def get_widget_generator(wid):
yield yield
async def get_widget_(wid: Widget): async def get_widget_(wid):
if obj := widget_map.get(wid): if obj := widget_map.get(wid):
return obj return obj
return await FakeAwaitable(get_widget_generator(wid)) return await FakeAwaitable(get_widget_generator(wid))
def widgets_wait_generator():
while True:
if Widget.widgets_completed:
return
yield
async def wait_for_widgets():
if Widget.widgets_completed:
return
await FakeAwaitable(widgets_wait_generator())
async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]:
if not config: if not config:
return [] return []
@ -273,6 +297,10 @@ async def set_obj_properties(w: Widget, config):
layout_type: str = layout[CONF_TYPE] layout_type: str = layout[CONF_TYPE]
add_lv_use(layout_type) add_lv_use(layout_type)
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
if (pad_row := layout.get(CONF_PAD_ROW)) is not None:
w.set_style(CONF_PAD_ROW, pad_row, 0)
if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None:
w.set_style(CONF_PAD_COLUMN, pad_column, 0)
if layout_type == TYPE_GRID: if layout_type == TYPE_GRID:
wid = config[CONF_ID] wid = config[CONF_ID]
rows = [str(x) for x in layout[CONF_GRID_ROWS]] rows = [str(x) for x in layout[CONF_GRID_ROWS]]
@ -299,8 +327,15 @@ async def set_obj_properties(w: Widget, config):
lv_obj.set_flex_align(w.obj, main, cross, track) lv_obj.set_flex_align(w.obj, main, cross, track)
parts = collect_parts(config) parts = collect_parts(config)
for part, states in parts.items(): for part, states in parts.items():
part = "LV_PART_" + part.upper()
for state, props in states.items(): for state, props in states.items():
lv_state = join_enums((f"LV_STATE_{state}", f"LV_PART_{part}")) state = "LV_STATE_" + state.upper()
if state == "LV_STATE_DEFAULT":
lv_state = literal(part)
elif part == "LV_PART_MAIN":
lv_state = literal(state)
else:
lv_state = join_enums((state, part))
for style_id in props.get(CONF_STYLES, ()): for style_id in props.get(CONF_STYLES, ()):
lv_obj.add_style(w.obj, MockObj(style_id), lv_state) lv_obj.add_style(w.obj, MockObj(style_id), lv_state)
for prop, value in { for prop, value in {
@ -313,11 +348,14 @@ async def set_obj_properties(w: Widget, config):
if group := config.get(CONF_GROUP): if group := config.get(CONF_GROUP):
group = await cg.get_variable(group) group = await cg.get_variable(group)
lv.group_add_obj(group, w.obj) lv.group_add_obj(group, w.obj)
flag_clr = set()
flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT] props = parts[CONF_MAIN][CONF_DEFAULT]
lambs = {}
flag_set = set()
flag_clr = set()
for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items():
if value: if isinstance(value, cv.Lambda):
lambs[prop] = value
elif value:
flag_set.add(prop) flag_set.add(prop)
else: else:
flag_clr.add(prop) flag_clr.add(prop)
@ -327,6 +365,13 @@ async def set_obj_properties(w: Widget, config):
if flag_clr: if flag_clr:
clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") clrs = join_enums(flag_clr, "LV_OBJ_FLAG_")
w.clear_flag(clrs) w.clear_flag(clrs)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
flag = f"LV_OBJ_FLAG_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_flag(flag)
cond.else_()
w.clear_flag(flag)
if states := config.get(CONF_STATE): if states := config.get(CONF_STATE):
adds = set() adds = set()
@ -348,11 +393,11 @@ async def set_obj_properties(w: Widget, config):
for key, value in lambs.items(): for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_) lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
state = f"LV_STATE_{key.upper()}" state = f"LV_STATE_{key.upper()}"
with LvConditional(f"{lamb}()") as cond: with LvConditional(call_lambda(lamb)) as cond:
w.add_state(state) w.add_state(state)
cond.else_() cond.else_()
w.clear_state(state) w.clear_state(state)
await w.set_property(CONF_SCROLLBAR_MODE, config) await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj")
async def add_widgets(parent: Widget, config: dict): async def add_widgets(parent: Widget, config: dict):

View file

@ -1,5 +1,6 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_GROUP,
CONF_MAX_VALUE, CONF_MAX_VALUE,
CONF_MIN_VALUE, CONF_MIN_VALUE,
CONF_MODE, CONF_MODE,
@ -20,7 +21,7 @@ from ..defines import (
literal, literal,
) )
from ..lv_validation import angle, get_start_value, lv_float from ..lv_validation import angle, get_start_value, lv_float
from ..lvcode import lv, lv_obj from ..lvcode import lv, lv_expr, lv_obj
from ..types import LvNumber, NumberType from ..types import LvNumber, NumberType
from . import Widget from . import Widget
@ -69,6 +70,9 @@ class ArcType(NumberType):
if config.get(CONF_ADJUSTABLE) is False: if config.get(CONF_ADJUSTABLE) is False:
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
w.clear_flag("LV_OBJ_FLAG_CLICKABLE") w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
elif CONF_GROUP not in config:
# For some reason arc does not get automatically added to the default group
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
value = await get_start_value(config) value = await get_start_value(config)
if value is not None: if value is not None:

View file

@ -3,7 +3,7 @@ import functools
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from ..defines import CONF_MAIN, literal from ..defines import CONF_MAIN
from ..lvcode import lv from ..lvcode import lv
from ..types import LvType from ..types import LvType
from . import Widget, WidgetType from . import Widget, WidgetType
@ -38,13 +38,15 @@ LINE_SCHEMA = {
class LineType(WidgetType): class LineType(WidgetType):
def __init__(self): def __init__(self):
super().__init__(CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA) super().__init__(
CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA, modify_schema={}
)
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
"""For a line object, create and add the points""" """For a line object, create and add the points"""
data = literal(config[CONF_POINTS]) if data := config.get(CONF_POINTS):
points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) points = cg.static_const_array(config[CONF_POINT_LIST_ID], data)
lv.line_set_points(w.obj, points, len(data)) lv.line_set_points(w.obj, points, len(data))
line_spec = LineType() line_spec = LineType()

View file

@ -13,7 +13,7 @@ from ..defines import (
TYPE_FLEX, TYPE_FLEX,
literal, literal,
) )
from ..helpers import add_lv_use from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import lv_bool, lv_pct, lv_text from ..lv_validation import lv_bool, lv_pct, lv_text
from ..lvcode import ( from ..lvcode import (
EVENT_ARG, EVENT_ARG,
@ -72,6 +72,7 @@ async def msgbox_to_code(conf):
*buttonmatrix_spec.get_uses(), *buttonmatrix_spec.get_uses(),
*button_spec.get_uses(), *button_spec.get_uses(),
) )
lvgl_components_required.add("BUTTONMATRIX")
messagebox_id = conf[CONF_ID] messagebox_id = conf[CONF_ID]
outer = lv_Pvariable(lv_obj_t, messagebox_id.id) outer = lv_Pvariable(lv_obj_t, messagebox_id.id)
buttonmatrix = new_Pvariable( buttonmatrix = new_Pvariable(

View file

@ -1,6 +1,7 @@
from esphome import automation, codegen as cg from esphome import automation, codegen as cg
from esphome.automation import Trigger
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
from ..defines import ( from ..defines import (
CONF_ANIMATION, CONF_ANIMATION,
@ -9,12 +10,39 @@ from ..defines import (
CONF_PAGE_WRAP, CONF_PAGE_WRAP,
CONF_SKIP, CONF_SKIP,
LV_ANIM, LV_ANIM,
literal,
) )
from ..lv_validation import lv_bool, lv_milliseconds from ..lv_validation import lv_bool, lv_milliseconds
from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp from ..lvcode import (
EVENT_ARG,
LVGL_COMP_ARG,
LambdaContext,
add_line_marks,
lv_add,
lvgl_comp,
)
from ..schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from ..types import LvglAction, lv_page_t from ..types import LvglAction, lv_page_t
from . import Widget, WidgetType, add_widgets, set_obj_properties from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
CONF_ON_LOAD = "on_load"
CONF_ON_UNLOAD = "on_unload"
PAGE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SKIP, default=False): lv_bool,
cv.Optional(CONF_ON_LOAD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()),
}
),
cv.Optional(CONF_ON_UNLOAD): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()),
}
),
}
)
class PageType(WidgetType): class PageType(WidgetType):
@ -23,9 +51,8 @@ class PageType(WidgetType):
CONF_PAGE, CONF_PAGE,
lv_page_t, lv_page_t,
(), (),
{ PAGE_SCHEMA,
cv.Optional(CONF_SKIP, default=False): lv_bool, modify_schema={},
},
) )
async def to_code(self, w: Widget, config: dict): async def to_code(self, w: Widget, config: dict):
@ -39,7 +66,6 @@ SHOW_SCHEMA = LVGL_SCHEMA.extend(
} }
) )
page_spec = PageType() page_spec = PageType()
@ -111,3 +137,21 @@ async def add_pages(lv_component, config):
await set_obj_properties(page, config) await set_obj_properties(page, config)
await set_obj_properties(page, pconf) await set_obj_properties(page, pconf)
await add_widgets(page, pconf) await add_widgets(page, pconf)
async def generate_page_triggers(lv_component, config):
for pconf in config.get(CONF_PAGES, ()):
page = (await get_widgets(pconf))[0]
for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD):
for loaded in pconf.get(ev, ()):
trigger = cg.new_Pvariable(loaded[CONF_TRIGGER_ID])
await automation.build_automation(trigger, [], loaded)
async with LambdaContext(EVENT_ARG, where=id) as context:
lv_add(trigger.trigger())
lv_add(
lv_component.add_event_cb(
page.obj,
await context.get_lambda(),
literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"),
)
)

View file

@ -27,6 +27,18 @@ enum MediaPlayerCommand : uint8_t {
}; };
const char *media_player_command_to_string(MediaPlayerCommand command); const char *media_player_command_to_string(MediaPlayerCommand command);
enum class MediaPlayerFormatPurpose : uint8_t {
PURPOSE_DEFAULT = 0,
PURPOSE_ANNOUNCEMENT = 1,
};
struct MediaPlayerSupportedFormat {
std::string format;
uint32_t sample_rate;
uint32_t num_channels;
MediaPlayerFormatPurpose purpose;
};
class MediaPlayer; class MediaPlayer;
class MediaPlayerTraits { class MediaPlayerTraits {
@ -37,8 +49,11 @@ class MediaPlayerTraits {
bool get_supports_pause() const { return this->supports_pause_; } bool get_supports_pause() const { return this->supports_pause_; }
std::vector<MediaPlayerSupportedFormat> &get_supported_formats() { return this->supported_formats_; }
protected: protected:
bool supports_pause_{false}; bool supports_pause_{false};
std::vector<MediaPlayerSupportedFormat> supported_formats_{};
}; };
class MediaPlayerCall { class MediaPlayerCall {

View file

@ -1,6 +1,9 @@
#pragma once #pragma once
#include "esphome/core/entity_base.h" #include <cstddef>
#include <cstdint>
#include <functional>
#include <vector>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome {

View file

@ -24,7 +24,11 @@ CONFIG_SCHEMA = cv.Schema(
esp32=False, esp32=False,
rp2040=False, rp2040=False,
): cv.All( ): cv.All(
cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]) cv.boolean,
cv.Any(
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
cv.boolean_false,
),
), ),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
} }

View file

@ -136,6 +136,9 @@ void Pipsolar::loop() {
if (this->output_source_priority_battery_switch_) { if (this->output_source_priority_battery_switch_) {
this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2);
} }
if (this->output_source_priority_hybrid_switch_) {
this->output_source_priority_hybrid_switch_->publish_state(value_output_source_priority_ == 3);
}
if (this->charger_source_priority_) { if (this->charger_source_priority_) {
this->charger_source_priority_->publish_state(value_charger_source_priority_); this->charger_source_priority_->publish_state(value_charger_source_priority_);
} }

View file

@ -174,6 +174,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI)
PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI)
PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI)
PIPSOLAR_SWITCH(output_source_priority_hybrid_switch, QPIRI)
PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI) PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI)
PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)

View file

@ -9,6 +9,7 @@ DEPENDENCIES = ["uart"]
CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility" CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility"
CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar" CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar"
CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery" CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery"
CONF_OUTPUT_SOURCE_PRIORITY_HYBRID = "output_source_priority_hybrid"
CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range"
CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel"
CONF_PV_POWER_BALANCE = "pv_power_balance" CONF_PV_POWER_BALANCE = "pv_power_balance"
@ -17,6 +18,7 @@ TYPES = {
CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None),
CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None),
CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None),
CONF_OUTPUT_SOURCE_PRIORITY_HYBRID: ("POP03", None),
CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"),
CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"),
CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"),

View file

@ -7,8 +7,10 @@
#include <hardware/clocks.h> #include <hardware/clocks.h>
#include <hardware/dma.h> #include <hardware/dma.h>
#include <hardware/irq.h>
#include <hardware/pio.h> #include <hardware/pio.h>
#include <pico/stdlib.h> #include <pico/stdlib.h>
#include <pico/sem.h>
namespace esphome { namespace esphome {
namespace rp2040_pio_led_strip { namespace rp2040_pio_led_strip {
@ -23,6 +25,19 @@ static std::map<Chipset, bool> conf_count_ = {
{CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false},
{CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false},
}; };
static bool dma_chan_active_[12];
static struct semaphore dma_write_complete_sem_[12];
// DMA interrupt service routine
void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() {
uint32_t channel = dma_hw->ints0;
for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) {
if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) {
dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt
sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt
}
}
}
void RP2040PIOLEDStripLightOutput::setup() { void RP2040PIOLEDStripLightOutput::setup() {
ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip...");
@ -57,22 +72,22 @@ void RP2040PIOLEDStripLightOutput::setup() {
// but there are only 4 state machines on each PIO so we can only have 4 strips per PIO // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO
uint offset = 0; uint offset = 0;
if (num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) {
ESP_LOGE(TAG, "Too many instances of PIO program"); ESP_LOGE(TAG, "Too many instances of PIO program");
this->mark_failed(); this->mark_failed();
return; return;
} }
// keep track of how many instances of the PIO program are running on each PIO // keep track of how many instances of the PIO program are running on each PIO
num_instance_[this->pio_ == pio0 ? 0 : 1]++; RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++;
// if there are multiple strips of the same chipset, we can reuse the same PIO program and save space // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space
if (this->conf_count_[this->chipset_]) { if (this->conf_count_[this->chipset_]) {
offset = chipset_offsets_[this->chipset_]; offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_];
} else { } else {
// Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it
offset = pio_add_program(this->pio_, this->program_); offset = pio_add_program(this->pio_, this->program_);
chipset_offsets_[this->chipset_] = offset; RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset;
conf_count_[this->chipset_] = true; RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true;
} }
// Configure the state machine's PIO, and start it // Configure the state machine's PIO, and start it
@ -93,6 +108,9 @@ void RP2040PIOLEDStripLightOutput::setup() {
return; return;
} }
// Mark the DMA channel as active
RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true;
this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); this->dma_config_ = dma_channel_get_default_config(this->dma_chan_);
channel_config_set_transfer_data_size( channel_config_set_transfer_data_size(
&this->dma_config_, &this->dma_config_,
@ -109,6 +127,13 @@ void RP2040PIOLEDStripLightOutput::setup() {
false // don't start yet false // don't start yet
); );
// Initialize the semaphore for this DMA channel
sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1);
irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt
dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt
irq_set_enabled(DMA_IRQ_0, true); // enable interrupt
this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_);
} }
@ -126,6 +151,7 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) {
} }
// the bits are already in the correct order for the pio program so we can just copy the buffer using DMA // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA
sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]);
dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_());
} }

View file

@ -13,6 +13,7 @@
#include <hardware/pio.h> #include <hardware/pio.h>
#include <hardware/structs/pio.h> #include <hardware/structs/pio.h>
#include <pico/stdio.h> #include <pico/stdio.h>
#include <pico/sem.h>
#include <map> #include <map>
namespace esphome { namespace esphome {
@ -95,6 +96,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight {
size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); }
static void dma_write_complete_handler_();
uint8_t *buf_{nullptr}; uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr}; uint8_t *effect_data_{nullptr};
@ -120,6 +123,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight {
inline static int num_instance_[2]; inline static int num_instance_[2];
inline static std::map<Chipset, bool> conf_count_; inline static std::map<Chipset, bool> conf_count_;
inline static std::map<Chipset, int> chipset_offsets_; inline static std::map<Chipset, int> chipset_offsets_;
inline static bool dma_chan_active_[12];
inline static struct semaphore dma_write_complete_sem_[12];
}; };
} // namespace rp2040_pio_led_strip } // namespace rp2040_pio_led_strip

View file

@ -32,7 +32,7 @@ void Rtttl::play(std::string rtttl) {
if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) {
int pos = this->rtttl_.find(':'); int pos = this->rtttl_.find(':');
auto name = this->rtttl_.substr(0, pos); auto name = this->rtttl_.substr(0, pos);
ESP_LOGW(TAG, "RTTL Component is already playing: %s", name.c_str()); ESP_LOGW(TAG, "RTTTL Component is already playing: %s", name.c_str());
return; return;
} }
@ -122,6 +122,7 @@ void Rtttl::stop() {
#ifdef USE_OUTPUT #ifdef USE_OUTPUT
if (this->output_ != nullptr) { if (this->output_ != nullptr) {
this->output_->set_level(0.0); this->output_->set_level(0.0);
this->set_state_(STATE_STOPPED);
} }
#endif #endif
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
@ -129,10 +130,10 @@ void Rtttl::stop() {
if (this->speaker_->is_running()) { if (this->speaker_->is_running()) {
this->speaker_->stop(); this->speaker_->stop();
} }
this->set_state_(STATE_STOPPING);
} }
#endif #endif
this->note_duration_ = 0; this->note_duration_ = 0;
this->set_state_(STATE_STOPPING);
} }
void Rtttl::loop() { void Rtttl::loop() {
@ -342,6 +343,7 @@ void Rtttl::finish_() {
#ifdef USE_OUTPUT #ifdef USE_OUTPUT
if (this->output_ != nullptr) { if (this->output_ != nullptr) {
this->output_->set_level(0.0); this->output_->set_level(0.0);
this->set_state_(State::STATE_STOPPED);
} }
#endif #endif
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
@ -354,9 +356,9 @@ void Rtttl::finish_() {
this->speaker_->play((uint8_t *) (&sample), 8); this->speaker_->play((uint8_t *) (&sample), 8);
this->speaker_->finish(); this->speaker_->finish();
this->set_state_(State::STATE_STOPPING);
} }
#endif #endif
this->set_state_(State::STATE_STOPPING);
this->note_duration_ = 0; this->note_duration_ = 0;
this->on_finished_playback_callback_.call(); this->on_finished_playback_callback_.call();
ESP_LOGD(TAG, "Playback finished"); ESP_LOGD(TAG, "Playback finished");

View file

@ -1,4 +1,5 @@
#include "socket.h" #include "socket.h"
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
#include <cerrno> #include <cerrno>
#include <cstring> #include <cstring>
#include <string> #include <string>
@ -74,3 +75,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po
} }
} // namespace socket } // namespace socket
} // namespace esphome } // namespace esphome
#endif

View file

@ -5,6 +5,7 @@
#include "esphome/core/optional.h" #include "esphome/core/optional.h"
#include "headers.h" #include "headers.h"
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
namespace esphome { namespace esphome {
namespace socket { namespace socket {
@ -57,3 +58,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po
} // namespace socket } // namespace socket
} // namespace esphome } // namespace esphome
#endif

View file

@ -1,5 +1,9 @@
#pragma once #pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace esphome { namespace esphome {
namespace speaker { namespace speaker {

View file

@ -15,6 +15,7 @@ CONF_DATAPOINT_TYPE = "datapoint_type"
CONF_STATUS_PIN = "status_pin" CONF_STATUS_PIN = "status_pin"
tuya_ns = cg.esphome_ns.namespace("tuya") tuya_ns = cg.esphome_ns.namespace("tuya")
TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True)
Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice)
DPTYPE_ANY = "any" DPTYPE_ANY = "any"

View file

@ -8,18 +8,36 @@ from esphome.const import (
CONF_MIN_VALUE, CONF_MIN_VALUE,
CONF_MULTIPLY, CONF_MULTIPLY,
CONF_STEP, CONF_STEP,
CONF_INITIAL_VALUE,
) )
from .. import tuya_ns, CONF_TUYA_ID, Tuya from .. import tuya_ns, CONF_TUYA_ID, Tuya, TuyaDatapointType
DEPENDENCIES = ["tuya"] DEPENDENCIES = ["tuya"]
CODEOWNERS = ["@frankiboy1"] CODEOWNERS = ["@frankiboy1"]
CONF_DATAPOINT_HIDDEN = "datapoint_hidden"
CONF_DATAPOINT_TYPE = "datapoint_type"
TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component) TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component)
DATAPOINT_TYPES = {
"int": TuyaDatapointType.INTEGER,
"uint": TuyaDatapointType.INTEGER,
"enum": TuyaDatapointType.ENUM,
}
def validate_min_max(config): def validate_min_max(config):
if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: max_value = config[CONF_MAX_VALUE]
min_value = config[CONF_MIN_VALUE]
if max_value <= min_value:
raise cv.Invalid("max_value must be greater than min_value") raise cv.Invalid("max_value must be greater than min_value")
if hidden_config := config.get(CONF_DATAPOINT_HIDDEN):
if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None:
if (initial_value > max_value) or (initial_value < min_value):
raise cv.Invalid(
f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}"
)
return config return config
@ -33,6 +51,16 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_,
cv.Required(CONF_STEP): cv.positive_float, cv.Required(CONF_STEP): cv.positive_float,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_DATAPOINT_HIDDEN): cv.All(
cv.Schema(
{
cv.Required(CONF_DATAPOINT_TYPE): cv.enum(
DATAPOINT_TYPES, lower=True
),
cv.Optional(CONF_INITIAL_VALUE): cv.float_,
}
)
),
} }
) )
.extend(cv.COMPONENT_SCHEMA), .extend(cv.COMPONENT_SCHEMA),
@ -56,3 +84,9 @@ async def to_code(config):
cg.add(var.set_tuya_parent(parent)) cg.add(var.set_tuya_parent(parent))
cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT]))
if hidden_config := config.get(CONF_DATAPOINT_HIDDEN):
cg.add(var.set_datapoint_type(hidden_config[CONF_DATAPOINT_TYPE]))
if (
hidden_init_value := hidden_config.get(CONF_INITIAL_VALUE, None)
) is not None:
cg.add(var.set_datapoint_initial_value(hidden_init_value))

View file

@ -15,8 +15,18 @@ void TuyaNumber::setup() {
ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum);
this->publish_state(datapoint.value_enum); this->publish_state(datapoint.value_enum);
} }
if ((this->type_) && (this->type_ != datapoint.type)) {
ESP_LOGW(TAG, "Reported type (%d) different than previously set (%d)!", static_cast<int>(datapoint.type),
static_cast<int>(*this->type_));
}
this->type_ = datapoint.type; this->type_ = datapoint.type;
}); });
this->parent_->add_on_initialized_callback([this] {
if ((this->initial_value_) && (this->type_)) {
this->control(*this->initial_value_);
}
});
} }
void TuyaNumber::control(float value) { void TuyaNumber::control(float value) {
@ -33,6 +43,15 @@ void TuyaNumber::control(float value) {
void TuyaNumber::dump_config() { void TuyaNumber::dump_config() {
LOG_NUMBER("", "Tuya Number", this); LOG_NUMBER("", "Tuya Number", this);
ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_); ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_);
if (this->type_) {
ESP_LOGCONFIG(TAG, " Datapoint type is %d", static_cast<int>(*this->type_));
} else {
ESP_LOGCONFIG(TAG, " Datapoint type is unknown");
}
if (this->initial_value_) {
ESP_LOGCONFIG(TAG, " Initial Value: %f", *this->initial_value_);
}
} }
} // namespace tuya } // namespace tuya

View file

@ -3,6 +3,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/tuya/tuya.h" #include "esphome/components/tuya/tuya.h"
#include "esphome/components/number/number.h" #include "esphome/components/number/number.h"
#include "esphome/core/optional.h"
namespace esphome { namespace esphome {
namespace tuya { namespace tuya {
@ -13,6 +14,8 @@ class TuyaNumber : public number::Number, public Component {
void dump_config() override; void dump_config() override;
void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } void set_number_id(uint8_t number_id) { this->number_id_ = number_id; }
void set_write_multiply(float factor) { multiply_by_ = factor; } void set_write_multiply(float factor) { multiply_by_ = factor; }
void set_datapoint_type(TuyaDatapointType type) { type_ = type; }
void set_datapoint_initial_value(float value) { this->initial_value_ = value; }
void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
@ -22,7 +25,8 @@ class TuyaNumber : public number::Number, public Component {
Tuya *parent_; Tuya *parent_;
uint8_t number_id_{0}; uint8_t number_id_{0};
float multiply_by_{1.0}; float multiply_by_{1.0};
TuyaDatapointType type_{}; optional<TuyaDatapointType> type_{};
optional<float> initial_value_{};
}; };
} // namespace tuya } // namespace tuya

View file

@ -480,7 +480,7 @@ void HOT WaveshareEPaperTypeA::display() {
this->start_data_(); this->start_data_();
switch (this->model_) { switch (this->model_) {
case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations
int16_t wb = ((this->get_width_internal()) >> 3); int16_t wb = ((this->get_width_controller()) >> 3);
for (int i = 0; i < this->get_height_internal(); i++) { for (int i = 0; i < this->get_height_internal(); i++) {
for (int j = 0; j < wb; j++) { for (int j = 0; j < wb; j++) {
int idx = j + (this->get_height_internal() - 1 - i) * wb; int idx = j + (this->get_height_internal() - 1 - i) * wb;
@ -766,7 +766,7 @@ void WaveshareEPaper2P7InV2::initialize() {
// XRAM_START_AND_END_POSITION // XRAM_START_AND_END_POSITION
this->command(0x44); this->command(0x44);
this->data(0x00); this->data(0x00);
this->data(((get_width_internal() - 1) >> 3) & 0xFF); this->data(((this->get_width_controller() - 1) >> 3) & 0xFF);
// YRAM_START_AND_END_POSITION // YRAM_START_AND_END_POSITION
this->command(0x45); this->command(0x45);
this->data(0x00); this->data(0x00);
@ -928,8 +928,8 @@ void HOT WaveshareEPaper2P7InB::display() {
// TCON_RESOLUTION // TCON_RESOLUTION
this->command(0x61); this->command(0x61);
this->data(this->get_width_internal() >> 8); this->data(this->get_width_controller() >> 8);
this->data(this->get_width_internal() & 0xff); // 176 this->data(this->get_width_controller() & 0xff); // 176
this->data(this->get_height_internal() >> 8); this->data(this->get_height_internal() >> 8);
this->data(this->get_height_internal() & 0xff); // 264 this->data(this->get_height_internal() & 0xff); // 264
@ -994,7 +994,7 @@ void WaveshareEPaper2P7InBV2::initialize() {
// self.SetWindows(0, 0, self.width-1, self.height-1) // self.SetWindows(0, 0, self.width-1, self.height-1)
// SetWindows(self, Xstart, Ystart, Xend, Yend): // SetWindows(self, Xstart, Ystart, Xend, Yend):
uint32_t xend = this->get_width_internal() - 1; uint32_t xend = this->get_width_controller() - 1;
uint32_t yend = this->get_height_internal() - 1; uint32_t yend = this->get_height_internal() - 1;
this->command(0x44); this->command(0x44);
this->data(0x00); this->data(0x00);

View file

@ -370,6 +370,20 @@ def boolean(value):
) )
def boolean_false(value):
"""Validate the given config option to be a boolean, set to False.
This option allows a bunch of different ways of expressing boolean values:
- instance of boolean
- 'true'/'false'
- 'yes'/'no'
- 'enable'/disable
"""
if boolean(value):
raise Invalid("Expected boolean value to be false")
return False
@schema_extractor_list @schema_extractor_list
def ensure_list(*validators): def ensure_list(*validators):
"""Validate this configuration option to be a list. """Validate this configuration option to be a list.
@ -1850,7 +1864,7 @@ def maybe_simple_value(*validators, **kwargs):
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return (validator, key) return (validator, key)
if isinstance(value, dict): if isinstance(value, dict) and key in value:
return validator(value) return validator(value)
return validator({key: value}) return validator({key: value})

View file

@ -431,6 +431,7 @@ CONF_LIGHT_ID = "light_id"
CONF_LIGHTNING_ENERGY = "lightning_energy" CONF_LIGHTNING_ENERGY = "lightning_energy"
CONF_LIGHTNING_THRESHOLD = "lightning_threshold" CONF_LIGHTNING_THRESHOLD = "lightning_threshold"
CONF_LIMIT_MODE = "limit_mode" CONF_LIMIT_MODE = "limit_mode"
CONF_LINE_FREQUENCY = "line_frequency"
CONF_LINE_THICKNESS = "line_thickness" CONF_LINE_THICKNESS = "line_thickness"
CONF_LINE_TYPE = "line_type" CONF_LINE_TYPE = "line_type"
CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOADED_INTEGRATIONS = "loaded_integrations"
@ -1042,6 +1043,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR"
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh"
UNIT_KILOWATT = "kW" UNIT_KILOWATT = "kW"
UNIT_KILOWATT_HOURS = "kWh" UNIT_KILOWATT_HOURS = "kWh"
UNIT_LITRE = "L"
UNIT_LUX = "lx" UNIT_LUX = "lx"
UNIT_METER = "m" UNIT_METER = "m"
UNIT_METER_PER_SECOND_SQUARED = "m/s²" UNIT_METER_PER_SECOND_SQUARED = "m/s²"

View file

@ -1,19 +1,64 @@
#include "bytebuffer.h" #include "bytebuffer.h"
#include <cassert> #include <cassert>
#include <cstring>
namespace esphome { namespace esphome {
ByteBuffer ByteBuffer::create(size_t capacity) { ByteBuffer ByteBuffer::wrap(const uint8_t *ptr, size_t len, Endian endianness) {
std::vector<uint8_t> data(capacity); // there is a double copy happening here, could be optimized but at cost of clarity.
return {data};
}
ByteBuffer ByteBuffer::wrap(uint8_t *ptr, size_t len) {
std::vector<uint8_t> data(ptr, ptr + len); std::vector<uint8_t> data(ptr, ptr + len);
return {data}; ByteBuffer buffer = {data};
buffer.endianness_ = endianness;
return buffer;
} }
ByteBuffer ByteBuffer::wrap(std::vector<uint8_t> data) { return {std::move(data)}; } ByteBuffer ByteBuffer::wrap(std::vector<uint8_t> const &data, Endian endianness) {
ByteBuffer buffer = {data};
buffer.endianness_ = endianness;
return buffer;
}
ByteBuffer ByteBuffer::wrap(uint8_t value) {
ByteBuffer buffer = ByteBuffer(1);
buffer.put_uint8(value);
buffer.flip();
return buffer;
}
ByteBuffer ByteBuffer::wrap(uint16_t value, Endian endianness) {
ByteBuffer buffer = ByteBuffer(2, endianness);
buffer.put_uint16(value);
buffer.flip();
return buffer;
}
ByteBuffer ByteBuffer::wrap(uint32_t value, Endian endianness) {
ByteBuffer buffer = ByteBuffer(4, endianness);
buffer.put_uint32(value);
buffer.flip();
return buffer;
}
ByteBuffer ByteBuffer::wrap(uint64_t value, Endian endianness) {
ByteBuffer buffer = ByteBuffer(8, endianness);
buffer.put_uint64(value);
buffer.flip();
return buffer;
}
ByteBuffer ByteBuffer::wrap(float value, Endian endianness) {
ByteBuffer buffer = ByteBuffer(sizeof(float), endianness);
buffer.put_float(value);
buffer.flip();
return buffer;
}
ByteBuffer ByteBuffer::wrap(double value, Endian endianness) {
ByteBuffer buffer = ByteBuffer(sizeof(double), endianness);
buffer.put_double(value);
buffer.flip();
return buffer;
}
void ByteBuffer::set_limit(size_t limit) { void ByteBuffer::set_limit(size_t limit) {
assert(limit <= this->get_capacity()); assert(limit <= this->get_capacity());
@ -27,108 +72,102 @@ void ByteBuffer::clear() {
this->limit_ = this->get_capacity(); this->limit_ = this->get_capacity();
this->position_ = 0; this->position_ = 0;
} }
uint16_t ByteBuffer::get_uint16() { void ByteBuffer::flip() {
assert(this->get_remaining() >= 2); this->limit_ = this->position_;
uint16_t value; this->position_ = 0;
if (endianness_ == LITTLE) {
value = this->data_[this->position_++];
value |= this->data_[this->position_++] << 8;
} else {
value = this->data_[this->position_++] << 8;
value |= this->data_[this->position_++];
}
return value;
} }
uint32_t ByteBuffer::get_uint32() { /// Getters
assert(this->get_remaining() >= 4);
uint32_t value;
if (endianness_ == LITTLE) {
value = this->data_[this->position_++];
value |= this->data_[this->position_++] << 8;
value |= this->data_[this->position_++] << 16;
value |= this->data_[this->position_++] << 24;
} else {
value = this->data_[this->position_++] << 24;
value |= this->data_[this->position_++] << 16;
value |= this->data_[this->position_++] << 8;
value |= this->data_[this->position_++];
}
return value;
}
uint32_t ByteBuffer::get_uint24() {
assert(this->get_remaining() >= 3);
uint32_t value;
if (endianness_ == LITTLE) {
value = this->data_[this->position_++];
value |= this->data_[this->position_++] << 8;
value |= this->data_[this->position_++] << 16;
} else {
value = this->data_[this->position_++] << 16;
value |= this->data_[this->position_++] << 8;
value |= this->data_[this->position_++];
}
return value;
}
uint32_t ByteBuffer::get_int24() {
auto value = this->get_uint24();
uint32_t mask = (~(uint32_t) 0) << 23;
if ((value & mask) != 0)
value |= mask;
return value;
}
uint8_t ByteBuffer::get_uint8() { uint8_t ByteBuffer::get_uint8() {
assert(this->get_remaining() >= 1); assert(this->get_remaining() >= 1);
return this->data_[this->position_++]; return this->data_[this->position_++];
} }
float ByteBuffer::get_float() { uint64_t ByteBuffer::get_uint(size_t length) {
auto value = this->get_uint32(); assert(this->get_remaining() >= length);
return *(float *) &value; uint64_t value = 0;
if (this->endianness_ == LITTLE) {
this->position_ += length;
auto index = this->position_;
while (length-- != 0) {
value <<= 8;
value |= this->data_[--index];
}
} else {
while (length-- != 0) {
value <<= 8;
value |= this->data_[this->position_++];
}
}
return value;
} }
uint32_t ByteBuffer::get_int24() {
auto value = this->get_uint24();
uint32_t mask = (~static_cast<uint32_t>(0)) << 23;
if ((value & mask) != 0)
value |= mask;
return value;
}
float ByteBuffer::get_float() {
assert(this->get_remaining() >= sizeof(float));
auto ui_value = this->get_uint32();
float value;
memcpy(&value, &ui_value, sizeof(float));
return value;
}
double ByteBuffer::get_double() {
assert(this->get_remaining() >= sizeof(double));
auto ui_value = this->get_uint64();
double value;
memcpy(&value, &ui_value, sizeof(double));
return value;
}
std::vector<uint8_t> ByteBuffer::get_vector(size_t length) {
assert(this->get_remaining() >= length);
auto start = this->data_.begin() + this->position_;
this->position_ += length;
return {start, start + length};
}
/// Putters
void ByteBuffer::put_uint8(uint8_t value) { void ByteBuffer::put_uint8(uint8_t value) {
assert(this->get_remaining() >= 1); assert(this->get_remaining() >= 1);
this->data_[this->position_++] = value; this->data_[this->position_++] = value;
} }
void ByteBuffer::put_uint16(uint16_t value) { void ByteBuffer::put_uint(uint64_t value, size_t length) {
assert(this->get_remaining() >= 2); assert(this->get_remaining() >= length);
if (this->endianness_ == LITTLE) { if (this->endianness_ == LITTLE) {
this->data_[this->position_++] = (uint8_t) value; while (length-- != 0) {
this->data_[this->position_++] = (uint8_t) (value >> 8); this->data_[this->position_++] = static_cast<uint8_t>(value);
value >>= 8;
}
} else { } else {
this->data_[this->position_++] = (uint8_t) (value >> 8); this->position_ += length;
this->data_[this->position_++] = (uint8_t) value; auto index = this->position_;
while (length-- != 0) {
this->data_[--index] = static_cast<uint8_t>(value);
value >>= 8;
}
} }
} }
void ByteBuffer::put_uint24(uint32_t value) { void ByteBuffer::put_float(float value) {
assert(this->get_remaining() >= 3); static_assert(sizeof(float) == sizeof(uint32_t), "Float sizes other than 32 bit not supported");
if (this->endianness_ == LITTLE) { assert(this->get_remaining() >= sizeof(float));
this->data_[this->position_++] = (uint8_t) value; uint32_t ui_value;
this->data_[this->position_++] = (uint8_t) (value >> 8); memcpy(&ui_value, &value, sizeof(float)); // this work-around required to silence compiler warnings
this->data_[this->position_++] = (uint8_t) (value >> 16); this->put_uint32(ui_value);
} else {
this->data_[this->position_++] = (uint8_t) (value >> 16);
this->data_[this->position_++] = (uint8_t) (value >> 8);
this->data_[this->position_++] = (uint8_t) value;
}
} }
void ByteBuffer::put_uint32(uint32_t value) { void ByteBuffer::put_double(double value) {
assert(this->get_remaining() >= 4); static_assert(sizeof(double) == sizeof(uint64_t), "Double sizes other than 64 bit not supported");
if (this->endianness_ == LITTLE) { assert(this->get_remaining() >= sizeof(double));
this->data_[this->position_++] = (uint8_t) value; uint64_t ui_value;
this->data_[this->position_++] = (uint8_t) (value >> 8); memcpy(&ui_value, &value, sizeof(double));
this->data_[this->position_++] = (uint8_t) (value >> 16); this->put_uint64(ui_value);
this->data_[this->position_++] = (uint8_t) (value >> 24);
} else {
this->data_[this->position_++] = (uint8_t) (value >> 24);
this->data_[this->position_++] = (uint8_t) (value >> 16);
this->data_[this->position_++] = (uint8_t) (value >> 8);
this->data_[this->position_++] = (uint8_t) value;
}
} }
void ByteBuffer::put_float(float value) { this->put_uint32(*(uint32_t *) &value); } void ByteBuffer::put_vector(const std::vector<uint8_t> &value) {
void ByteBuffer::flip() { assert(this->get_remaining() >= value.size());
this->limit_ = this->position_; std::copy(value.begin(), value.end(), this->data_.begin() + this->position_);
this->position_ = 0; this->position_ += value.size();
} }
} // namespace esphome } // namespace esphome

View file

@ -15,55 +15,103 @@ enum Endian { LITTLE, BIG };
* *
* There are three variables maintained pointing into the buffer: * There are three variables maintained pointing into the buffer:
* *
* 0 <= position <= limit <= capacity * capacity: the maximum amount of data that can be stored - set on construction and cannot be changed
*
* capacity: the maximum amount of data that can be stored
* limit: the limit of the data currently available to get or put * limit: the limit of the data currently available to get or put
* position: the current insert or extract position * position: the current insert or extract position
* *
* 0 <= position <= limit <= capacity
*
* In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore
* the position to the mark. * the position to the mark.
* *
* The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order. * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order.
* *
* The flip() operation will reset the position to 0 and limit to the current position. This is useful for reading
* data from a buffer after it has been written.
*
*/ */
class ByteBuffer { class ByteBuffer {
public: public:
// Default constructor (compatibility with TEMPLATABLE_VALUE)
ByteBuffer() : ByteBuffer(std::vector<uint8_t>()) {}
/** /**
* Create a new Bytebuffer with the given capacity * Create a new Bytebuffer with the given capacity
*/ */
static ByteBuffer create(size_t capacity); ByteBuffer(size_t capacity, Endian endianness = LITTLE)
: data_(std::vector<uint8_t>(capacity)), endianness_(endianness), limit_(capacity){};
/** /**
* Wrap an existing vector in a Bytebufffer * Wrap an existing vector in a ByteBufffer
*/ */
static ByteBuffer wrap(std::vector<uint8_t> data); static ByteBuffer wrap(std::vector<uint8_t> const &data, Endian endianness = LITTLE);
/** /**
* Wrap an existing array in a Bytebufffer * Wrap an existing array in a ByteBuffer. Note that this will create a copy of the data.
*/ */
static ByteBuffer wrap(uint8_t *ptr, size_t len); static ByteBuffer wrap(const uint8_t *ptr, size_t len, Endian endianness = LITTLE);
// Convenience functions to create a ByteBuffer from a value
static ByteBuffer wrap(uint8_t value);
static ByteBuffer wrap(uint16_t value, Endian endianness = LITTLE);
static ByteBuffer wrap(uint32_t value, Endian endianness = LITTLE);
static ByteBuffer wrap(uint64_t value, Endian endianness = LITTLE);
static ByteBuffer wrap(int8_t value) { return wrap(static_cast<uint8_t>(value)); }
static ByteBuffer wrap(int16_t value, Endian endianness = LITTLE) {
return wrap(static_cast<uint16_t>(value), endianness);
}
static ByteBuffer wrap(int32_t value, Endian endianness = LITTLE) {
return wrap(static_cast<uint32_t>(value), endianness);
}
static ByteBuffer wrap(int64_t value, Endian endianness = LITTLE) {
return wrap(static_cast<uint64_t>(value), endianness);
}
static ByteBuffer wrap(float value, Endian endianness = LITTLE);
static ByteBuffer wrap(double value, Endian endianness = LITTLE);
static ByteBuffer wrap(bool value) { return wrap(static_cast<uint8_t>(value)); }
// Get an integral value from the buffer, increment position by length
uint64_t get_uint(size_t length);
// Get one byte from the buffer, increment position by 1 // Get one byte from the buffer, increment position by 1
uint8_t get_uint8(); uint8_t get_uint8();
// Get a 16 bit unsigned value, increment by 2 // Get a 16 bit unsigned value, increment by 2
uint16_t get_uint16(); uint16_t get_uint16() { return static_cast<uint16_t>(this->get_uint(sizeof(uint16_t))); };
// Get a 24 bit unsigned value, increment by 3 // Get a 24 bit unsigned value, increment by 3
uint32_t get_uint24(); uint32_t get_uint24() { return static_cast<uint32_t>(this->get_uint(3)); };
// Get a 32 bit unsigned value, increment by 4 // Get a 32 bit unsigned value, increment by 4
uint32_t get_uint32(); uint32_t get_uint32() { return static_cast<uint32_t>(this->get_uint(sizeof(uint32_t))); };
// signed versions of the get functions // Get a 64 bit unsigned value, increment by 8
uint8_t get_int8() { return (int8_t) this->get_uint8(); }; uint64_t get_uint64() { return this->get_uint(sizeof(uint64_t)); };
int16_t get_int16() { return (int16_t) this->get_uint16(); } // Signed versions of the get functions
uint8_t get_int8() { return static_cast<int8_t>(this->get_uint8()); };
int16_t get_int16() { return static_cast<int16_t>(this->get_uint(sizeof(int16_t))); }
uint32_t get_int24(); uint32_t get_int24();
int32_t get_int32() { return (int32_t) this->get_uint32(); } int32_t get_int32() { return static_cast<int32_t>(this->get_uint(sizeof(int32_t))); }
int64_t get_int64() { return static_cast<int64_t>(this->get_uint(sizeof(int64_t))); }
// Get a float value, increment by 4 // Get a float value, increment by 4
float get_float(); float get_float();
// Get a double value, increment by 8
double get_double();
// Get a bool value, increment by 1
bool get_bool() { return this->get_uint8(); }
// Get vector of bytes, increment by length
std::vector<uint8_t> get_vector(size_t length);
// put values into the buffer, increment the position accordingly // Put values into the buffer, increment the position accordingly
// put any integral value, length represents the number of bytes
void put_uint(uint64_t value, size_t length);
void put_uint8(uint8_t value); void put_uint8(uint8_t value);
void put_uint16(uint16_t value); void put_uint16(uint16_t value) { this->put_uint(value, sizeof(uint16_t)); }
void put_uint24(uint32_t value); void put_uint24(uint32_t value) { this->put_uint(value, 3); }
void put_uint32(uint32_t value); void put_uint32(uint32_t value) { this->put_uint(value, sizeof(uint32_t)); }
void put_uint64(uint64_t value) { this->put_uint(value, sizeof(uint64_t)); }
// Signed versions of the put functions
void put_int8(int8_t value) { this->put_uint8(static_cast<uint8_t>(value)); }
void put_int16(int32_t value) { this->put_uint(static_cast<uint16_t>(value), sizeof(uint16_t)); }
void put_int24(int32_t value) { this->put_uint(static_cast<uint32_t>(value), 3); }
void put_int32(int32_t value) { this->put_uint(static_cast<uint32_t>(value), sizeof(uint32_t)); }
void put_int64(int64_t value) { this->put_uint(static_cast<uint64_t>(value), sizeof(uint64_t)); }
// Extra put functions
void put_float(float value); void put_float(float value);
void put_double(double value);
void put_bool(bool value) { this->put_uint8(value); }
void put_vector(const std::vector<uint8_t> &value);
inline size_t get_capacity() const { return this->data_.size(); } inline size_t get_capacity() const { return this->data_.size(); }
inline size_t get_position() const { return this->position_; } inline size_t get_position() const { return this->position_; }
@ -80,12 +128,12 @@ class ByteBuffer {
// set limit to current position, postition to zero. Used when swapping from write to read operations. // set limit to current position, postition to zero. Used when swapping from write to read operations.
void flip(); void flip();
// retrieve a pointer to the underlying data. // retrieve a pointer to the underlying data.
uint8_t *array() { return this->data_.data(); }; std::vector<uint8_t> get_data() { return this->data_; };
void rewind() { this->position_ = 0; } void rewind() { this->position_ = 0; }
void reset() { this->position_ = this->mark_; } void reset() { this->position_ = this->mark_; }
protected: protected:
ByteBuffer(std::vector<uint8_t> data) : data_(std::move(data)) { this->limit_ = this->get_capacity(); } ByteBuffer(std::vector<uint8_t> const &data) : data_(data), limit_(data.size()) {}
std::vector<uint8_t> data_; std::vector<uint8_t> data_;
Endian endianness_{LITTLE}; Endian endianness_{LITTLE};
size_t position_{0}; size_t position_{0};

View file

@ -106,6 +106,8 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
return True return True
if old.build_path != new.build_path: if old.build_path != new.build_path:
return True return True
if old.loaded_integrations != new.loaded_integrations:
return True
return False return False
@ -117,7 +119,9 @@ def update_storage_json():
return return
if storage_should_clean(old, new): if storage_should_clean(old, new):
_LOGGER.info("Core config or version changed, cleaning build files...") _LOGGER.info(
"Core config, version or integrations changed, cleaning build files..."
)
clean_build() clean_build()
new.save(path) new.save(path)

View file

@ -153,6 +153,13 @@ build_flags =
-DUSE_ESP32_FRAMEWORK_ESP_IDF -DUSE_ESP32_FRAMEWORK_ESP_IDF
extra_scripts = post:esphome/components/esp32/post_build.py.script extra_scripts = post:esphome/components/esp32/post_build.py.script
; This are common settings for the ESP32 using the latest ESP-IDF version.
[common:esp32-idf-5_3]
extends = common:esp32-idf
platform = platformio/espressif32@6.8.0
platform_packages =
platformio/framework-espidf@~3.50300.0
; These are common settings for the RP2040 using Arduino. ; These are common settings for the RP2040 using Arduino.
[common:rp2040-arduino] [common:rp2040-arduino]
extends = common:arduino extends = common:arduino
@ -229,6 +236,15 @@ build_flags =
${flags:runtime.build_flags} ${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32 -DUSE_ESP32_VARIANT_ESP32
[env:esp32-idf-5_3]
extends = common:esp32-idf-5_3
board = esp32dev
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf
build_flags =
${common:esp32-idf.build_flags}
${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32
[env:esp32-idf-tidy] [env:esp32-idf-tidy]
extends = common:esp32-idf extends = common:esp32-idf
board = esp32dev board = esp32dev
@ -265,6 +281,15 @@ build_flags =
${flags:runtime.build_flags} ${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32C3 -DUSE_ESP32_VARIANT_ESP32C3
[env:esp32c3-idf-5_3]
extends = common:esp32-idf-5_3
board = esp32-c3-devkitm-1
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf
build_flags =
${common:esp32-idf.build_flags}
${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32C3
[env:esp32c3-idf-tidy] [env:esp32c3-idf-tidy]
extends = common:esp32-idf extends = common:esp32-idf
board = esp32-c3-devkitm-1 board = esp32-c3-devkitm-1
@ -301,6 +326,15 @@ build_flags =
${flags:runtime.build_flags} ${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32S2 -DUSE_ESP32_VARIANT_ESP32S2
[env:esp32s2-idf-5_3]
extends = common:esp32-idf-5_3
board = esp32-s2-kaluga-1
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf
build_flags =
${common:esp32-idf.build_flags}
${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32S2
[env:esp32s2-idf-tidy] [env:esp32s2-idf-tidy]
extends = common:esp32-idf extends = common:esp32-idf
board = esp32-s2-kaluga-1 board = esp32-s2-kaluga-1
@ -337,6 +371,15 @@ build_flags =
${flags:runtime.build_flags} ${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32S3 -DUSE_ESP32_VARIANT_ESP32S3
[env:esp32s3-idf-5_3]
extends = common:esp32-idf-5_3
board = esp32-s3-devkitc-1
board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf
build_flags =
${common:esp32-idf.build_flags}
${flags:runtime.build_flags}
-DUSE_ESP32_VARIANT_ESP32S3
[env:esp32s3-idf-tidy] [env:esp32s3-idf-tidy]
extends = common:esp32-idf extends = common:esp32-idf
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1

View file

@ -0,0 +1,62 @@
uart:
- id: uart_bl0906
tx_pin:
number: ${tx_pin}
rx_pin:
number: ${rx_pin}
baud_rate: 19200
sensor:
- platform: bl0906
frequency:
name: 'Frequency'
temperature:
name: 'Temperature'
voltage:
name: 'Voltage'
channel_1:
current:
name: 'Current_1'
power:
name: 'Power_1'
energy:
name: 'Energy_1'
channel_2:
current:
name: 'Current_2'
power:
name: 'Power_2'
energy:
name: 'Energy_2'
channel_3:
current:
name: 'Current_3'
power:
name: 'Power_3'
energy:
name: 'Energy_3'
channel_4:
current:
name: 'Current_4'
power:
name: 'Power_4'
energy:
name: 'Energy_4'
channel_5:
current:
name: 'Current_5'
power:
name: 'Power_5'
energy:
name: 'Energy_5'
channel_6:
current:
name: 'Current_6'
power:
name: 'Power_6'
energy:
name: 'Energy_6'
total_energy:
name: 'Total_Energy'
total_power:
name: 'Total_Power'

View file

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO12
rx_pin: GPIO14
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO7
rx_pin: GPIO8
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO7
rx_pin: GPIO8
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO12
rx_pin: GPIO14
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO1
rx_pin: GPIO3
<<: !include common.yaml

View file

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View file

@ -0,0 +1,22 @@
uart:
- id: uart_bl0942
tx_pin:
number: TX1
rx_pin:
number: RX1
baud_rate: 2400
sensor:
- platform: bl0942
address: 0
line_frequency: 50Hz
voltage:
name: BL0942 Voltage
current:
name: BL0942 Current
power:
name: BL0942 Power
energy:
name: BL0942 Energy
frequency:
name: BL0942 Frequency

View file

@ -19,6 +19,7 @@ display:
lambda: |- lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height()); it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: ili9xxx - platform: ili9xxx
invert_colors: false
dimensions: dimensions:
width: 320 width: 320
height: 240 height: 240

View file

@ -20,6 +20,7 @@ display:
lambda: |- lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height()); it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: ili9xxx - platform: ili9xxx
invert_colors: false
dimensions: dimensions:
width: 320 width: 320
height: 240 height: 240

View file

@ -20,6 +20,7 @@ display:
lambda: |- lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height()); it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: ili9xxx - platform: ili9xxx
invert_colors: false
dimensions: dimensions:
width: 320 width: 320
height: 240 height: 240

View file

@ -20,6 +20,7 @@ display:
lambda: |- lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height()); it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: ili9xxx - platform: ili9xxx
invert_colors: false
dimensions: dimensions:
width: 320 width: 320
height: 240 height: 240

View file

@ -20,6 +20,7 @@ display:
lambda: |- lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height()); it.rectangle(0, 0, it.get_width(), it.get_height());
- platform: ili9xxx - platform: ili9xxx
invert_colors: false
dimensions: dimensions:
width: 320 width: 320
height: 240 height: 240

View file

@ -127,3 +127,11 @@ binary_sensor:
- platform: lvgl - platform: lvgl
name: LVGL checkbox name: LVGL checkbox
widget: checkbox_id widget: checkbox_id
wifi:
ssid: SSID
password: PASSWORD123
time:
platform: sntp
id: time_id

View file

@ -1,6 +1,8 @@
lvgl: lvgl:
log_level: TRACE log_level: TRACE
bg_color: light_blue bg_color: light_blue
disp_bg_color: 0xffff00
disp_bg_image: cat_image
theme: theme:
obj: obj:
border_width: 1 border_width: 1
@ -16,8 +18,6 @@ lvgl:
border_width: 0 border_width: 0
radius: 0 radius: 0
pad_all: 0 pad_all: 0
pad_row: 0
pad_column: 0
border_color: 0x0077b3 border_color: 0x0077b3
text_color: 0xFFFFFF text_color: 0xFFFFFF
width: 100% width: 100%
@ -54,7 +54,25 @@ lvgl:
long_press_time: 500ms long_press_time: 500ms
pages: pages:
- id: page1 - id: page1
on_load:
- logger.log: page loaded
- lvgl.widget.focus:
action: restore
on_unload:
- logger.log: page unloaded
- lvgl.widget.focus: mark
on_all_events:
logger.log:
format: "Event %s"
args: ['lv_event_code_name_for(event->code).c_str()']
skip: true skip: true
layout:
type: flex
pad_row: 4
pad_column: 4px
flex_align_main: center
flex_align_cross: start
flex_align_track: end
widgets: widgets:
- animimg: - animimg:
height: 60 height: 60
@ -63,6 +81,10 @@ lvgl:
repeat_count: 10 repeat_count: 10
duration: 1s duration: 1s
auto_start: true auto_start: true
on_all_events:
logger.log:
format: "Event %s"
args: ['lv_event_code_name_for(event->code).c_str()']
- label: - label:
id: hello_label id: hello_label
text: Hello world text: Hello world
@ -73,6 +95,9 @@ lvgl:
on_click: on_click:
then: then:
- lvgl.animimg.stop: anim_img - lvgl.animimg.stop: anim_img
- lvgl.update:
disp_bg_color: 0xffff00
disp_bg_image: cat_image
- label: - label:
text: "Hello shiny day" text: "Hello shiny day"
text_color: 0xFFFFFF text_color: 0xFFFFFF
@ -118,10 +143,8 @@ lvgl:
outline_width: 10px outline_width: 10px
pad_all: 10px pad_all: 10px
pad_bottom: 10px pad_bottom: 10px
pad_column: 10px
pad_left: 10px pad_left: 10px
pad_right: 10px pad_right: 10px
pad_row: 10px
pad_top: 10px pad_top: 10px
shadow_color: light_blue shadow_color: light_blue
shadow_ofs_x: 5 shadow_ofs_x: 5
@ -221,10 +244,57 @@ lvgl:
- label: - label:
text: Button text: Button
on_click: on_click:
lvgl.label.update: - lvgl.widget.focus: spin_up
id: hello_label - lvgl.widget.focus: next
bg_color: 0x123456 - lvgl.widget.focus: previous
text: clicked - lvgl.widget.focus:
action: previous
freeze: true
- lvgl.widget.focus:
id: spin_up
freeze: true
editing: true
- lvgl.label.update:
id: hello_label
bg_color: 0x123456
text: clicked
- lvgl.label.update:
id: hello_label
text: !lambda return "hello world";
- lvgl.label.update:
id: hello_label
text: !lambda |-
ESP_LOGD("label", "multi-line lambda");
return "hello world";
- lvgl.label.update:
id: hello_label
text: !lambda 'return str_sprintf("Hello space");'
- lvgl.label.update:
id: hello_label
text:
format: "sprintf format %s"
args: ['x ? "checked" : "unchecked"']
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
time: time_id
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
time: !lambda return id(time_id).now();
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
time: !lambda |-
ESP_LOGD("label", "multi-line lambda");
return id(time_id).now();
on_value: on_value:
logger.log: logger.log:
format: "state now %d" format: "state now %d"
@ -264,6 +334,17 @@ lvgl:
src: cat_image src: cat_image
align: top_left align: top_left
y: 50 y: 50
- tileview:
id: tileview_id
scrollbar_mode: active
tiles:
- id: page_1
row: 0
column: 0
dir: HOR
widgets:
- obj:
bg_color: 0x000000
- id: page2 - id: page2
widgets: widgets:
@ -339,6 +420,7 @@ lvgl:
format: "bar value %f" format: "bar value %f"
args: [x] args: [x]
- line: - line:
id: lv_line_id
align: center align: center
points: points:
- 5, 5 - 5, 5
@ -347,7 +429,10 @@ lvgl:
- 180, 60 - 180, 60
- 240, 10 - 240, 10
on_click: on_click:
lvgl.page.next: - lvgl.widget.update:
id: lv_line_id
line_color: 0xFFFF
- lvgl.page.next:
- switch: - switch:
align: right_mid align: right_mid
- checkbox: - checkbox:
@ -396,6 +481,8 @@ lvgl:
grid_row_align: end grid_row_align: end
grid_rows: [25px, fr(1), content] grid_rows: [25px, fr(1), content]
grid_columns: [40, fr(1), fr(1)] grid_columns: [40, fr(1), fr(1)]
pad_row: 6px
pad_column: 0
widgets: widgets:
- image: - image:
grid_cell_row_pos: 0 grid_cell_row_pos: 0

View file

@ -0,0 +1,4 @@
substitutions:
network_enable_ipv6: "false"
<<: !include common.yaml

View file

@ -220,6 +220,8 @@ switch:
name: inverter0_output_source_priority_solar name: inverter0_output_source_priority_solar
output_source_priority_battery: output_source_priority_battery:
name: inverter0_output_source_priority_battery name: inverter0_output_source_priority_battery
output_source_priority_hybrid:
name: inverter0_output_source_priority_hybrid
input_voltage_range: input_voltage_range:
name: inverter0_input_voltage_range name: inverter0_input_voltage_range
pv_ok_condition_for_parallel: pv_ok_condition_for_parallel:

View file

@ -220,6 +220,8 @@ switch:
name: inverter0_output_source_priority_solar name: inverter0_output_source_priority_solar
output_source_priority_battery: output_source_priority_battery:
name: inverter0_output_source_priority_battery name: inverter0_output_source_priority_battery
output_source_priority_hybrid:
name: inverter0_output_source_priority_hybrid
input_voltage_range: input_voltage_range:
name: inverter0_input_voltage_range name: inverter0_input_voltage_range
pv_ok_condition_for_parallel: pv_ok_condition_for_parallel:

View file

@ -220,6 +220,8 @@ switch:
name: inverter0_output_source_priority_solar name: inverter0_output_source_priority_solar
output_source_priority_battery: output_source_priority_battery:
name: inverter0_output_source_priority_battery name: inverter0_output_source_priority_battery
output_source_priority_hybrid:
name: inverter0_output_source_priority_hybrid
input_voltage_range: input_voltage_range:
name: inverter0_input_voltage_range name: inverter0_input_voltage_range
pv_ok_condition_for_parallel: pv_ok_condition_for_parallel:

View file

@ -220,6 +220,8 @@ switch:
name: inverter0_output_source_priority_solar name: inverter0_output_source_priority_solar
output_source_priority_battery: output_source_priority_battery:
name: inverter0_output_source_priority_battery name: inverter0_output_source_priority_battery
output_source_priority_hybrid:
name: inverter0_output_source_priority_hybrid
input_voltage_range: input_voltage_range:
name: inverter0_input_voltage_range name: inverter0_input_voltage_range
pv_ok_condition_for_parallel: pv_ok_condition_for_parallel:

View file

@ -220,6 +220,8 @@ switch:
name: inverter0_output_source_priority_solar name: inverter0_output_source_priority_solar
output_source_priority_battery: output_source_priority_battery:
name: inverter0_output_source_priority_battery name: inverter0_output_source_priority_battery
output_source_priority_hybrid:
name: inverter0_output_source_priority_hybrid
input_voltage_range: input_voltage_range:
name: inverter0_input_voltage_range name: inverter0_input_voltage_range
pv_ok_condition_for_parallel: pv_ok_condition_for_parallel:

View file

@ -220,6 +220,8 @@ switch:
name: inverter0_output_source_priority_solar name: inverter0_output_source_priority_solar
output_source_priority_battery: output_source_priority_battery:
name: inverter0_output_source_priority_battery name: inverter0_output_source_priority_battery
output_source_priority_hybrid:
name: inverter0_output_source_priority_hybrid
input_voltage_range: input_voltage_range:
name: inverter0_input_voltage_range name: inverter0_input_voltage_range
pv_ok_condition_for_parallel: pv_ok_condition_for_parallel: