Merge branch 'esphome:dev' into feature-max6921

This commit is contained in:
endym 2024-08-27 17:18:06 +02:00 committed by GitHub
commit 8b01633d49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
224 changed files with 2956 additions and 535 deletions

View file

@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v6.7.0
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
@ -69,7 +69,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.6.1 uses: docker/build-push-action@v6.7.0
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile

View file

@ -9,6 +9,7 @@ on:
paths: paths:
- "**" - "**"
- "!.github/workflows/*.yml" - "!.github/workflows/*.yml"
- "!.github/actions/build-image/*"
- ".github/workflows/ci.yml" - ".github/workflows/ci.yml"
- "!.yamllint" - "!.yamllint"
- "!.github/dependabot.yml" - "!.github/dependabot.yml"
@ -396,7 +397,7 @@ jobs:
file: ${{ fromJson(needs.list-components.outputs.components) }} file: ${{ fromJson(needs.list-components.outputs.components) }}
steps: steps:
- name: Install dependencies - name: Install dependencies
run: sudo apt-get install libsodium-dev libsdl2-dev run: sudo apt-get install libsdl2-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
@ -450,7 +451,7 @@ jobs:
run: echo ${{ matrix.components }} run: echo ${{ matrix.components }}
- name: Install dependencies - name: Install dependencies
run: sudo apt-get install libsodium-dev libsdl2-dev run: sudo apt-get install libsdl2-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7

View file

@ -46,6 +46,7 @@ esphome/components/async_tcp/* @OttoWinter
esphome/components/at581x/* @X-Ryl669 esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner esphome/components/atm90e26/* @danieltwagner
esphome/components/atm90e32/* @circuitsetup @descipher
esphome/components/b_parasite/* @rbaron esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan esphome/components/ballu/* @bazuchan
esphome/components/bang_bang/* @OttoWinter esphome/components/bang_bang/* @OttoWinter
@ -168,7 +169,10 @@ esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
esphome/components/homeassistant/* @OttoWinter esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @OttoWinter @esphome/core
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004
esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywell_hih_i2c/* @Benichou34
esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp/* @RubyBailey
esphome/components/honeywellabp2_i2c/* @jpfaff esphome/components/honeywellabp2_i2c/* @jpfaff
@ -454,6 +458,7 @@ esphome/components/wl_134/* @hobbypunk90
esphome/components/x9c/* @EtienneMD esphome/components/x9c/* @EtienneMD
esphome/components/xgzp68xx/* @gcormier esphome/components/xgzp68xx/* @gcormier
esphome/components/xiaomi_hhccjcy10/* @fariouche esphome/components/xiaomi_hhccjcy10/* @fariouche
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs

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

@ -14,8 +14,6 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
ESP_LOGD(TAG, "version = %d", value->version); ESP_LOGD(TAG, "version = %d", value->version);
if (value->version == 1) { if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
if (this->humidity_sensor_ != nullptr) { if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(value->humidity / 2.0f); this->humidity_sensor_->publish_state(value->humidity / 2.0f);
} }
@ -43,6 +41,10 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc); this->tvoc_sensor_->publish_state(value->voc);
} }
if (this->illuminance_sensor_ != nullptr) {
this->illuminance_sensor_->publish_state(value->ambientLight);
}
} else { } else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
} }
@ -68,6 +70,7 @@ void AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
} }
AirthingsWavePlus::AirthingsWavePlus() { AirthingsWavePlus::AirthingsWavePlus() {

View file

@ -22,6 +22,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
protected: protected:
bool is_valid_radon_value_(uint16_t radon); bool is_valid_radon_value_(uint16_t radon);
@ -32,6 +33,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *illuminance_sensor_{nullptr};
struct WavePlusReadings { struct WavePlusReadings {
uint8_t version; uint8_t version;

View file

@ -12,6 +12,9 @@ from esphome.const import (
CONF_CO2, CONF_CO2,
UNIT_BECQUEREL_PER_CUBIC_METER, UNIT_BECQUEREL_PER_CUBIC_METER,
UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_MILLION,
CONF_ILLUMINANCE,
UNIT_LUX,
DEVICE_CLASS_ILLUMINANCE,
) )
DEPENDENCIES = airthings_wave_base.DEPENDENCIES DEPENDENCIES = airthings_wave_base.DEPENDENCIES
@ -45,6 +48,12 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
device_class=DEVICE_CLASS_CARBON_DIOXIDE, device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_LUX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
} }
) )
@ -62,3 +71,6 @@ async def to_code(config):
if config_co2 := config.get(CONF_CO2): if config_co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(config_co2) sens = await sensor.new_sensor(config_co2)
cg.add(var.set_co2(sens)) cg.add(var.set_co2(sens))
if config_illuminance := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(config_illuminance)
cg.add(var.set_illuminance(sens))

View file

@ -155,7 +155,7 @@ async def to_code(config):
decoded = base64.b64decode(encryption_config[CONF_KEY]) decoded = base64.b64decode(encryption_config[CONF_KEY])
cg.add(var.set_noise_psk(list(decoded))) cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE") cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.4") cg.add_library("esphome/noise-c", "0.1.6")
else: else:
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")

View file

@ -686,6 +686,7 @@ message SubscribeHomeAssistantStateResponse {
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
string entity_id = 1; string entity_id = 1;
string attribute = 2; string attribute = 2;
bool once = 3;
} }
message HomeAssistantStateResponse { message HomeAssistantStateResponse {
@ -1106,6 +1107,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;
@ -1121,6 +1135,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

@ -179,6 +179,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_++;
} }
@ -1025,6 +1026,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) {
@ -1335,6 +1345,9 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
case enums::UPDATE_COMMAND_CHECK: case enums::UPDATE_COMMAND_CHECK:
update->check(); update->check();
break; break;
case enums::UPDATE_COMMAND_NONE:
ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled. Check client is sending the correct command");
break;
default: default:
ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command); ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command);
break; break;

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) {
@ -3109,6 +3121,16 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
out.append("SubscribeHomeAssistantStatesRequest {}"); out.append("SubscribeHomeAssistantStatesRequest {}");
} }
#endif #endif
bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->once = value.as_bool();
return true;
}
default:
return false;
}
}
bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: { case 1: {
@ -3126,6 +3148,7 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto
void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->entity_id); buffer.encode_string(1, this->entity_id);
buffer.encode_string(2, this->attribute); buffer.encode_string(2, this->attribute);
buffer.encode_bool(3, this->once);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
@ -3138,6 +3161,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
out.append(" attribute: "); out.append(" attribute: ");
out.append("'").append(this->attribute).append("'"); out.append("'").append(this->attribute).append("'");
out.append("\n"); out.append("\n");
out.append(" once: ");
out.append(YESNO(this->once));
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -5108,6 +5135,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: {
@ -5144,6 +5229,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;
} }
@ -5167,6 +5256,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 {
@ -5204,6 +5296,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,
@ -836,6 +840,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
public: public:
std::string entity_id{}; std::string entity_id{};
std::string attribute{}; std::string attribute{};
bool once{false};
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;
@ -843,6 +848,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class HomeAssistantStateResponse : public ProtoMessage { class HomeAssistantStateResponse : public ProtoMessage {
public: public:
@ -1265,6 +1271,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{};
@ -1275,6 +1296,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

@ -311,6 +311,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());
@ -1135,6 +1143,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_update_command_request(msg); this->on_update_command_request(msg);
#endif
break;
}
case 119: {
#ifdef USE_MEDIA_PLAYER
MediaPlayerSupportedFormat msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_media_player_supported_format: %s", msg.dump().c_str());
#endif
this->on_media_player_supported_format(msg);
#endif #endif
break; break;
} }

View file

@ -145,6 +145,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

@ -359,8 +359,18 @@ void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<s
.entity_id = std::move(entity_id), .entity_id = std::move(entity_id),
.attribute = std::move(attribute), .attribute = std::move(attribute),
.callback = std::move(f), .callback = std::move(f),
.once = false,
}); });
} }
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const { const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_; return this->state_subs_;
} }

View file

@ -112,10 +112,13 @@ class APIServer : public Component, public Controller {
std::string entity_id; std::string entity_id;
optional<std::string> attribute; optional<std::string> attribute;
std::function<void(std::string)> callback; std::function<void(std::string)> callback;
bool once;
}; };
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f); std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }

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

@ -0,0 +1,7 @@
import esphome.codegen as cg
CODEOWNERS = ["@circuitsetup", "@descipher"]
atm90e32_ns = cg.esphome_ns.namespace("atm90e32")
CONF_ATM90E32_ID = "atm90e32_id"

View file

@ -132,10 +132,77 @@ void ATM90E32Component::update() {
this->status_clear_warning(); this->status_clear_warning();
} }
void ATM90E32Component::restore_calibrations_() {
if (enable_offset_calibration_) {
this->pref_.load(&this->offset_phase_);
}
};
void ATM90E32Component::run_offset_calibrations() {
// Run the calibrations and
// Setup voltage and current calibration offsets for PHASE A
this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
// Setup voltage and current calibration offsets for PHASE B
this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
// Setup voltage and current calibration offsets for PHASE C
this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
this->pref_.save(&this->offset_phase_);
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
}
void ATM90E32Component::clear_offset_calibrations() {
// Clear the calibrations and
this->offset_phase_[PHASEA].voltage_offset_ = 0;
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEA].current_offset_ = 0;
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
this->offset_phase_[PHASEB].voltage_offset_ = 0;
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEB].current_offset_ = 0;
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
this->offset_phase_[PHASEC].voltage_offset_ = 0;
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEC].current_offset_ = 0;
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
this->pref_.save(&this->offset_phase_);
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
}
void ATM90E32Component::setup() { void ATM90E32Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component...");
this->spi_setup(); this->spi_setup();
if (this->enable_offset_calibration_) {
uint32_t hash = fnv1_hash(App.get_friendly_name());
this->pref_ = global_preferences->make_preference<Calibration[3]>(hash, true);
this->restore_calibrations_();
}
uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t mmode0 = 0x87; // 3P4W 50Hz
if (line_freq_ == 60) { if (line_freq_ == 60) {
mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz
@ -167,27 +234,12 @@ void ATM90E32Component::setup() {
this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10%
// Setup voltage and current calibration offsets for PHASE A
this->phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // A Voltage offset
this->phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // A Current offset
// Setup voltage and current gain for PHASE A // Setup voltage and current gain for PHASE A
this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain
// Setup voltage and current calibration offsets for PHASE B
this->phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // B Voltage offset
this->phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // B Current offset
// Setup voltage and current gain for PHASE B // Setup voltage and current gain for PHASE B
this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain
// Setup voltage and current calibration offsets for PHASE C
this->phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
// Setup voltage and current gain for PHASE C // Setup voltage and current gain for PHASE C
this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain
this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain

View file

@ -1,9 +1,12 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "atm90e32_reg.h" #include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
namespace esphome { namespace esphome {
namespace atm90e32 { namespace atm90e32 {
@ -20,7 +23,6 @@ class ATM90E32Component : public PollingComponent,
void dump_config() override; void dump_config() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void update() override; void update() override;
void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; }
void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; }
void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; }
@ -48,9 +50,11 @@ class ATM90E32Component : public PollingComponent,
void set_line_freq(int freq) { line_freq_ = freq; } void set_line_freq(int freq) { line_freq_ = freq; }
void set_current_phases(int phases) { current_phases_ = phases; } void set_current_phases(int phases) { current_phases_ = phases; }
void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
void run_offset_calibrations();
void clear_offset_calibrations();
void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; }
uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/);
uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); uint16_t calibrate_current_offset_phase(uint8_t /*phase*/);
int32_t last_periodic_millis = millis(); int32_t last_periodic_millis = millis();
protected: protected:
@ -83,10 +87,11 @@ class ATM90E32Component : public PollingComponent,
float get_chip_temperature_(); float get_chip_temperature_();
bool get_publish_interval_flag_() { return publish_interval_flag_; }; bool get_publish_interval_flag_() { return publish_interval_flag_; };
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
void restore_calibrations_();
struct ATM90E32Phase { struct ATM90E32Phase {
uint16_t voltage_gain_{7305}; uint16_t voltage_gain_{0};
uint16_t ct_gain_{27961}; uint16_t ct_gain_{0};
uint16_t voltage_offset_{0}; uint16_t voltage_offset_{0};
uint16_t current_offset_{0}; uint16_t current_offset_{0};
float voltage_{0}; float voltage_{0};
@ -114,13 +119,21 @@ class ATM90E32Component : public PollingComponent,
uint32_t cumulative_reverse_active_energy_{0}; uint32_t cumulative_reverse_active_energy_{0};
} phase_[3]; } phase_[3];
struct Calibration {
uint16_t voltage_offset_{0};
uint16_t current_offset_{0};
} offset_phase_[3];
ESPPreferenceObject pref_;
sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *freq_sensor_{nullptr};
sensor::Sensor *chip_temperature_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr};
uint16_t pga_gain_{0x15}; uint16_t pga_gain_{0x15};
int line_freq_{60}; int line_freq_{60};
int current_phases_{3}; int current_phases_{3};
bool publish_interval_flag_{true}; bool publish_interval_flag_{false};
bool peak_current_signed_{false}; bool peak_current_signed_{false};
bool enable_offset_calibration_{false};
}; };
} // namespace atm90e32 } // namespace atm90e32

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <cinttypes>
namespace esphome { namespace esphome {
namespace atm90e32 { namespace atm90e32 {

View file

@ -0,0 +1,43 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE
from .. import atm90e32_ns
from ..sensor import ATM90E32Component
CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration"
CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
ATM90E32CalibrationButton = atm90e32_ns.class_(
"ATM90E32CalibrationButton",
button.Button,
)
ATM90E32ClearCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearCalibrationButton",
button.Button,
)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
ATM90E32CalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
),
}
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
b = await button.new_button(run_offset)
await cg.register_parented(b, parent)
if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
b = await button.new_button(clear_offset)
await cg.register_parented(b, parent)

View file

@ -0,0 +1,20 @@
#include "atm90e32_button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace atm90e32 {
static const char *const TAG = "atm90e32.button";
void ATM90E32CalibrationButton::press_action() {
ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process...");
this->parent_->run_offset_calibrations();
}
void ATM90E32ClearCalibrationButton::press_action() {
ESP_LOGI(TAG, "Offset calibrations cleared.");
this->parent_->clear_offset_calibrations();
}
} // namespace atm90e32
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/atm90e32/atm90e32.h"
#include "esphome/components/button/button.h"
namespace esphome {
namespace atm90e32 {
class ATM90E32CalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32CalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearCalibrationButton() = default;
protected:
void press_action() override;
};
} // namespace atm90e32
} // namespace esphome

View file

@ -1,21 +1,22 @@
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_APPARENT_POWER,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
CONF_CURRENT, CONF_CURRENT,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_LINE_FREQUENCY,
CONF_PHASE_A, CONF_PHASE_A,
CONF_PHASE_ANGLE,
CONF_PHASE_B, CONF_PHASE_B,
CONF_PHASE_C, CONF_PHASE_C,
CONF_PHASE_ANGLE,
CONF_POWER, CONF_POWER,
CONF_POWER_FACTOR, CONF_POWER_FACTOR,
CONF_APPARENT_POWER, CONF_REACTIVE_POWER,
CONF_FREQUENCY,
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,
@ -23,13 +24,13 @@ from esphome.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_DIAGNOSTIC,
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_AMPERE,
UNIT_DEGREES,
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_DEGREES,
UNIT_HERTZ, UNIT_HERTZ,
UNIT_VOLT, UNIT_VOLT,
UNIT_VOLT_AMPS_REACTIVE, UNIT_VOLT_AMPS_REACTIVE,
@ -37,7 +38,8 @@ from esphome.const import (
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
CONF_LINE_FREQUENCY = "line_frequency" from . import atm90e32_ns
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"
@ -46,6 +48,7 @@ CONF_GAIN_CT = "gain_ct"
CONF_HARMONIC_POWER = "harmonic_power" CONF_HARMONIC_POWER = "harmonic_power"
CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT = "peak_current"
CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed"
CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration"
UNIT_DEG = "degrees" UNIT_DEG = "degrees"
LINE_FREQS = { LINE_FREQS = {
"50HZ": 50, "50HZ": 50,
@ -61,7 +64,6 @@ PGA_GAINS = {
"4X": 0x2A, "4X": 0x2A,
} }
atm90e32_ns = cg.esphome_ns.namespace("atm90e32")
ATM90E32Component = atm90e32_ns.class_( ATM90E32Component = atm90e32_ns.class_(
"ATM90E32Component", cg.PollingComponent, spi.SPIDevice "ATM90E32Component", cg.PollingComponent, spi.SPIDevice
) )
@ -164,6 +166,7 @@ CONFIG_SCHEMA = (
), ),
cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True),
cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean,
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
@ -227,3 +230,4 @@ async def to_code(config):
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))
cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION]))

View file

@ -90,7 +90,7 @@ struct BedjetStatusPacket {
int unused_6 : 1; // 0x4 int unused_6 : 1; // 0x4
bool is_dual_zone : 1; /// Is part of a Dual Zone configuration bool is_dual_zone : 1; /// Is part of a Dual Zone configuration
int unused_7 : 1; // 0x1 int unused_7 : 1; // 0x1
} dual_zone_flags; } dual_zone_flags; // NOLINT(clang-diagnostic-unaligned-access)
uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310 uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310
uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310 uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310

View file

@ -18,10 +18,11 @@ class BinaryLightOutput : public light::LightOutput {
void write_state(light::LightState *state) override { void write_state(light::LightState *state) override {
bool binary; bool binary;
state->current_values_as_binary(&binary); state->current_values_as_binary(&binary);
if (binary) if (binary) {
this->output_->turn_on(); this->output_->turn_on();
else } else {
this->output_->turn_off(); this->output_->turn_off();
}
} }
protected: protected:

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

@ -1,4 +1,5 @@
#include "captive_portal.h" #include "captive_portal.h"
#ifdef USE_CAPTIVE_PORTAL
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/components/wifi/wifi_component.h" #include "esphome/components/wifi/wifi_component.h"
@ -91,3 +92,4 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo
} // namespace captive_portal } // namespace captive_portal
} // namespace esphome } // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_CAPTIVE_PORTAL
#include <memory> #include <memory>
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <DNSServer.h> #include <DNSServer.h>
@ -71,3 +72,4 @@ extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-
} // namespace captive_portal } // namespace captive_portal
} // namespace esphome } // namespace esphome
#endif

View file

@ -16,10 +16,11 @@ class DemoSensor : public sensor::Sensor, public PollingComponent {
float base = std::isnan(this->state) ? 0.0f : this->state; float base = std::isnan(this->state) ? 0.0f : this->state;
this->publish_state(base + val * 10); this->publish_state(base + val * 10);
} else { } else {
if (val < 0.1) if (val < 0.1) {
this->publish_state(NAN); this->publish_state(NAN);
else } else {
this->publish_state(val * 100); this->publish_state(val * 100);
}
} }
} }
}; };

View file

@ -1,4 +1,5 @@
#include "e131.h" #include "e131.h"
#ifdef USE_NETWORK
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -118,3 +119,4 @@ bool E131Component::process_(int universe, const E131Packet &packet) {
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -53,3 +54,4 @@ class E131Component : public esphome::Component {
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "e131.h" #include "e131.h"
#ifdef USE_NETWORK
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@ -90,3 +91,4 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif

View file

@ -2,7 +2,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/light/addressable_light_effect.h" #include "esphome/components/light/addressable_light_effect.h"
#ifdef USE_NETWORK
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@ -42,3 +42,4 @@ class E131AddressableLightEffect : public light::AddressableLightEffect {
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#include <cstring> #include <cstring>
#include "e131.h" #include "e131.h"
#ifdef USE_NETWORK
#include "esphome/components/network/ip_address.h" #include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
@ -137,3 +138,4 @@ bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E13
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif

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

@ -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,15 @@ 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), "WS2812": LEDStripTimings(400, 1000, 1000, 400, 0, 0),
"SK6812": LEDStripTimings(300, 900, 600, 600), "SK6812": LEDStripTimings(300, 900, 600, 600, 0, 0),
"APA106": LEDStripTimings(350, 1360, 1360, 350), "APA106": LEDStripTimings(350, 1360, 1360, 350, 0, 0),
"SM16703": LEDStripTimings(300, 900, 900, 300), "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0),
} }
@ -58,6 +60,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 +92,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 +125,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 +136,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,5 +1,5 @@
#include "ota_esphome.h" #include "ota_esphome.h"
#ifdef USE_OTA
#include "esphome/components/md5/md5.h" #include "esphome/components/md5/md5.h"
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend.h"
@ -410,3 +410,4 @@ float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::A
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
} // namespace esphome } // namespace esphome
#endif

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_OTA
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend.h"
@ -41,3 +42,4 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
}; };
} // namespace esphome } // namespace esphome
#endif

View file

@ -307,7 +307,7 @@ void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) {
void FingerprintGrowComponent::delete_all_fingerprints() { void FingerprintGrowComponent::delete_all_fingerprints() {
ESP_LOGI(TAG, "Deleting all stored fingerprints"); ESP_LOGI(TAG, "Deleting all stored fingerprints");
this->data_ = {EMPTY}; this->data_ = {DELETE_ALL};
switch (this->send_command_()) { switch (this->send_command_()) {
case OK: case OK:
ESP_LOGI(TAG, "Deleted all fingerprints"); ESP_LOGI(TAG, "Deleted all fingerprints");

View file

@ -36,7 +36,7 @@ enum GrowCommand {
LOAD = 0x07, LOAD = 0x07,
UPLOAD = 0x08, UPLOAD = 0x08,
DELETE = 0x0C, DELETE = 0x0C,
EMPTY = 0x0D, DELETE_ALL = 0x0D, // aka EMPTY
READ_SYS_PARAM = 0x0F, READ_SYS_PARAM = 0x0F,
SET_PASSWORD = 0x12, SET_PASSWORD = 0x12,
VERIFY_PASSWORD = 0x13, VERIFY_PASSWORD = 0x13,

View file

@ -80,8 +80,8 @@ class HaierClimateBase : public esphome::Component,
const char *phase_to_string_(ProtocolPhases phase); const char *phase_to_string_(ProtocolPhases phase);
virtual void set_handlers() = 0; virtual void set_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming)
virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming)
virtual void initialization(){}; virtual void initialization(){};
virtual bool prepare_pending_action(); virtual bool prepare_pending_action();
virtual void process_protocol_reset(); virtual void process_protocol_reset();

View file

@ -0,0 +1,2 @@
AUTO_LOAD = ["md5"]
CODEOWNERS = ["@dwmw2"]

View file

@ -0,0 +1,56 @@
#include <cstdio>
#include <cstring>
#include "hmac_md5.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace hmac_md5 {
void HmacMD5::init(const uint8_t *key, size_t len) {
uint8_t ipad[64], opad[64];
memset(ipad, 0, sizeof(ipad));
if (len > 64) {
md5::MD5Digest keymd5;
keymd5.init();
keymd5.add(key, len);
keymd5.calculate();
keymd5.get_bytes(ipad);
} else {
memcpy(ipad, key, len);
}
memcpy(opad, ipad, sizeof(opad));
for (int i = 0; i < 64; i++) {
ipad[i] ^= 0x36;
opad[i] ^= 0x5c;
}
this->ihash_.init();
this->ihash_.add(ipad, sizeof(ipad));
this->ohash_.init();
this->ohash_.add(opad, sizeof(opad));
}
void HmacMD5::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); }
void HmacMD5::calculate() {
uint8_t ibytes[16];
this->ihash_.calculate();
this->ihash_.get_bytes(ibytes);
this->ohash_.add(ibytes, sizeof(ibytes));
this->ohash_.calculate();
}
void HmacMD5::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); }
void HmacMD5::get_hex(char *output) { this->ohash_.get_hex(output); }
bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); }
bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); }
} // namespace hmac_md5
} // namespace esphome

View file

@ -0,0 +1,48 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/components/md5/md5.h"
#include <string>
namespace esphome {
namespace hmac_md5 {
class HmacMD5 {
public:
HmacMD5() = default;
~HmacMD5() = default;
/// Initialize a new MD5 digest computation.
void init(const uint8_t *key, size_t len);
void init(const char *key, size_t len) { this->init((const uint8_t *) key, len); }
void init(const std::string &key) { this->init(key.c_str(), key.length()); }
/// Add bytes of data for the digest.
void add(const uint8_t *data, size_t len);
void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); }
/// Compute the digest, based on the provided data.
void calculate();
/// Retrieve the HMAC-MD5 digest as bytes.
/// The output must be able to hold 16 bytes or more.
void get_bytes(uint8_t *output);
/// Retrieve the HMAC-MD5 digest as hex characters.
/// The output must be able to hold 32 bytes or more.
void get_hex(char *output);
/// Compare the digest against a provided byte-encoded digest (16 bytes).
bool equals_bytes(const uint8_t *expected);
/// Compare the digest against a provided hex-encoded digest (32 bytes).
bool equals_hex(const char *expected);
protected:
md5::MD5Digest ihash_;
md5::MD5Digest ohash_;
};
} // namespace hmac_md5
} // namespace esphome

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@OttoWinter", "@esphome/core"]
homeassistant_ns = cg.esphome_ns.namespace("homeassistant") homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema(
@ -13,6 +13,13 @@ HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema(
} }
) )
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema(
{
cv.Required(CONF_ENTITY_ID): cv.entity_id,
cv.Optional(CONF_INTERNAL, default=True): cv.boolean,
}
)
def setup_home_assistant_entity(var, config): def setup_home_assistant_entity(var, config):
cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))

View file

@ -0,0 +1,33 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
)
CODEOWNERS = ["@landonr"]
DEPENDENCIES = ["api"]
HomeassistantNumber = homeassistant_ns.class_(
"HomeassistantNumber", number.Number, cg.Component
)
CONFIG_SCHEMA = (
number.number_schema(HomeassistantNumber)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = await number.new_number(
config,
min_value=0,
max_value=0,
step=0,
)
await cg.register_component(var, config)
setup_home_assistant_entity(var, config)

View file

@ -0,0 +1,100 @@
#include "homeassistant_number.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
namespace esphome {
namespace homeassistant {
static const char *const TAG = "homeassistant.number";
void HomeassistantNumber::state_changed_(const std::string &state) {
auto number_value = parse_number<float>(state);
if (!number_value.has_value()) {
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str());
this->publish_state(NAN);
return;
}
if (this->state == number_value.value()) {
return;
}
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str());
this->publish_state(number_value.value());
}
void HomeassistantNumber::min_retrieved_(const std::string &min) {
auto min_value = parse_number<float>(min);
if (!min_value.has_value()) {
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str());
}
ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str());
this->traits.set_min_value(min_value.value());
}
void HomeassistantNumber::max_retrieved_(const std::string &max) {
auto max_value = parse_number<float>(max);
if (!max_value.has_value()) {
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str());
}
ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str());
this->traits.set_max_value(max_value.value());
}
void HomeassistantNumber::step_retrieved_(const std::string &step) {
auto step_value = parse_number<float>(step);
if (!step_value.has_value()) {
ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str());
}
ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str());
this->traits.set_step(step_value.value());
}
void HomeassistantNumber::setup() {
api::global_api_server->subscribe_home_assistant_state(
this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, optional<std::string>("min"),
std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, optional<std::string>("max"),
std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, optional<std::string>("step"),
std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
}
void HomeassistantNumber::dump_config() {
LOG_NUMBER("", "Homeassistant Number", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
}
float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
void HomeassistantNumber::control(float value) {
if (!api::global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients connected to API server");
return;
}
this->publish_state(value);
api::HomeassistantServiceResponse resp;
resp.service = "number.set_value";
api::HomeassistantServiceMap entity_id;
entity_id.key = "entity_id";
entity_id.value = this->entity_id_;
resp.data.push_back(entity_id);
api::HomeassistantServiceMap entity_value;
entity_value.key = "value";
entity_value.value = to_string(value);
resp.data.push_back(entity_value);
api::global_api_server->send_homeassistant_service_call(resp);
}
} // namespace homeassistant
} // namespace esphome

View file

@ -0,0 +1,31 @@
#pragma once
#include <map>
#include <string>
#include "esphome/components/number/number.h"
#include "esphome/core/component.h"
namespace esphome {
namespace homeassistant {
class HomeassistantNumber : public number::Number, public Component {
public:
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
void state_changed_(const std::string &state);
void min_retrieved_(const std::string &min);
void max_retrieved_(const std::string &max);
void step_retrieved_(const std::string &step);
void control(float value) override;
std::string entity_id_;
};
} // namespace homeassistant
} // namespace esphome

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
)
CODEOWNERS = ["@Links2004"]
DEPENDENCIES = ["api"]
HomeassistantSwitch = homeassistant_ns.class_(
"HomeassistantSwitch", switch.Switch, cg.Component
)
CONFIG_SCHEMA = (
switch.switch_schema(HomeassistantSwitch)
.extend(cv.COMPONENT_SCHEMA)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await switch.register_switch(var, config)
setup_home_assistant_entity(var, config)

View file

@ -0,0 +1,59 @@
#include "homeassistant_switch.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
namespace esphome {
namespace homeassistant {
static const char *const TAG = "homeassistant.switch";
using namespace esphome::switch_;
void HomeassistantSwitch::setup() {
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) {
auto val = parse_on_off(state.c_str());
switch (val) {
case PARSE_NONE:
case PARSE_TOGGLE:
ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
break;
case PARSE_ON:
case PARSE_OFF:
bool new_state = val == PARSE_ON;
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
this->publish_state(new_state);
break;
}
});
}
void HomeassistantSwitch::dump_config() {
LOG_SWITCH("", "Homeassistant Switch", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
}
float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
void HomeassistantSwitch::write_state(bool state) {
if (!api::global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients connected to API server");
return;
}
api::HomeassistantServiceResponse resp;
if (state) {
resp.service = "switch.turn_on";
} else {
resp.service = "switch.turn_off";
}
api::HomeassistantServiceMap entity_id_kv;
entity_id_kv.key = "entity_id";
entity_id_kv.value = this->entity_id_;
resp.data.push_back(entity_id_kv);
api::global_api_server->send_homeassistant_service_call(resp);
}
} // namespace homeassistant
} // namespace esphome

View file

@ -0,0 +1,22 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
namespace esphome {
namespace homeassistant {
class HomeassistantSwitch : public switch_::Switch, public Component {
public:
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
void write_state(bool state) override;
std::string entity_id_;
};
} // namespace homeassistant
} // namespace esphome

View file

@ -1,15 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_MAC_ADDRESS,
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_HOST, PLATFORM_HOST,
CONF_MAC_ADDRESS,
) )
from esphome.core import CORE from esphome.core import CORE
from esphome.helpers import IS_MACOS
import esphome.config_validation as cv
import esphome.codegen as cg
from .const import KEY_HOST from .const import KEY_HOST
@ -42,8 +41,5 @@ async def to_code(config):
cg.add_build_flag("-DUSE_HOST") cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=c++17") cg.add_build_flag("-std=c++17")
cg.add_build_flag("-lsodium")
if IS_MACOS:
cg.add_build_flag("-L/opt/homebrew/lib")
cg.add_define("ESPHOME_BOARD", "host") cg.add_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("platform", "platformio/native")

View file

@ -180,7 +180,11 @@ void I2SAudioSpeaker::player_task(void *params) {
} }
} }
void I2SAudioSpeaker::stop() { void I2SAudioSpeaker::stop() { this->stop_(false); }
void I2SAudioSpeaker::finish() { this->stop_(true); }
void I2SAudioSpeaker::stop_(bool wait_on_empty) {
if (this->is_failed()) if (this->is_failed())
return; return;
if (this->state_ == speaker::STATE_STOPPED) if (this->state_ == speaker::STATE_STOPPED)
@ -192,7 +196,11 @@ void I2SAudioSpeaker::stop() {
this->state_ = speaker::STATE_STOPPING; this->state_ = speaker::STATE_STOPPING;
DataEvent data; DataEvent data;
data.stop = true; data.stop = true;
xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); if (wait_on_empty) {
xQueueSend(this->buffer_queue_, &data, portMAX_DELAY);
} else {
xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY);
}
} }
void I2SAudioSpeaker::watch_() { void I2SAudioSpeaker::watch_() {

View file

@ -53,6 +53,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
void start() override; void start() override;
void stop() override; void stop() override;
void finish() override;
size_t play(const uint8_t *data, size_t length) override; size_t play(const uint8_t *data, size_t length) override;
@ -60,6 +61,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
protected: protected:
void start_(); void start_();
void stop_(bool wait_on_empty);
void watch_(); void watch_();
static void player_task(void *params); static void player_task(void *params);

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

@ -1,5 +1,5 @@
#include "improv_serial_component.h" #include "improv_serial_component.h"
#ifdef USE_WIFI
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@ -313,3 +313,4 @@ ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidel
} // namespace improv_serial } // namespace improv_serial
} // namespace esphome } // namespace esphome
#endif

View file

@ -5,7 +5,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#ifdef USE_WIFI
#include <improv.h> #include <improv.h>
#include <vector> #include <vector>
@ -78,3 +78,4 @@ extern ImprovSerialComponent
} // namespace improv_serial } // namespace improv_serial
} // namespace esphome } // namespace esphome
#endif

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

@ -114,10 +114,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
if (now - this->last_add_ < this->add_led_interval_) if (now - this->last_add_ < this->add_led_interval_)
return; return;
this->last_add_ = now; this->last_add_ = now;
if (this->reverse_) if (this->reverse_) {
it.shift_left(1); it.shift_left(1);
else } else {
it.shift_right(1); it.shift_right(1);
}
const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_]; const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
Color esp_color = Color(color.r, color.g, color.b, color.w); Color esp_color = Color(color.r, color.g, color.b, color.w);
if (color.gradient) { if (color.gradient) {
@ -127,10 +128,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds); uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
esp_color = esp_color.gradient(next_esp_color, gradient); esp_color = esp_color.gradient(next_esp_color, gradient);
} }
if (this->reverse_) if (this->reverse_) {
it[-1] = esp_color; it[-1] = esp_color;
else } else {
it[0] = esp_color; it[0] = esp_color;
}
if (++this->leds_added_ >= color.num_leds) { if (++this->leds_added_ >= color.num_leds) {
this->leds_added_ = 0; this->leds_added_ = 0;
this->at_color_ = (this->at_color_ + 1) % this->colors_.size(); this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
@ -207,10 +209,11 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
const uint8_t sine = half_sin8(view.get_effect_data()); const uint8_t sine = half_sin8(view.get_effect_data());
view = current_color * sine; view = current_color * sine;
const uint8_t new_pos = view.get_effect_data() + pos_add; const uint8_t new_pos = view.get_effect_data() + pos_add;
if (new_pos < view.get_effect_data()) if (new_pos < view.get_effect_data()) {
view.set_effect_data(0); view.set_effect_data(0);
else } else {
view.set_effect_data(new_pos); view.set_effect_data(new_pos);
}
} else { } else {
view = Color::BLACK; view = Color::BLACK;
} }
@ -254,10 +257,11 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine); view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine);
} }
const uint8_t new_x = x + pos_add; const uint8_t new_x = x + pos_add;
if (new_x > 0b11111) if (new_x > 0b11111) {
view.set_effect_data(0); view.set_effect_data(0);
else } else {
view.set_effect_data((new_x << 3) | color); view.set_effect_data((new_x << 3) | color);
}
} else { } else {
view = Color(0, 0, 0, 0); view = Color(0, 0, 0, 0);
} }

View file

@ -7,6 +7,8 @@
namespace esphome { namespace esphome {
namespace light { namespace light {
enum class LimitMode { CLAMP, DO_NOTHING };
template<typename... Ts> class ToggleAction : public Action<Ts...> { template<typename... Ts> class ToggleAction : public Action<Ts...> {
public: public:
explicit ToggleAction(LightState *state) : state_(state) {} explicit ToggleAction(LightState *state) : state_(state) {}
@ -77,7 +79,10 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
float rel = this->relative_brightness_.value(x...); float rel = this->relative_brightness_.value(x...);
float cur; float cur;
this->parent_->remote_values.as_brightness(&cur); this->parent_->remote_values.as_brightness(&cur);
float new_brightness = clamp(cur + rel, 0.0f, 1.0f); if ((limit_mode_ == LimitMode::DO_NOTHING) && ((cur < min_brightness_) || (cur > max_brightness_))) {
return;
}
float new_brightness = clamp(cur + rel, min_brightness_, max_brightness_);
call.set_state(new_brightness != 0.0f); call.set_state(new_brightness != 0.0f);
call.set_brightness(new_brightness); call.set_brightness(new_brightness);
@ -85,8 +90,18 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
call.perform(); call.perform();
} }
void set_min_max_brightness(float min, float max) {
this->min_brightness_ = min;
this->max_brightness_ = max;
}
void set_limit_mode(LimitMode limit_mode) { this->limit_mode_ = limit_mode; }
protected: protected:
LightState *parent_; LightState *parent_;
float min_brightness_{0.0};
float max_brightness_{1.0};
LimitMode limit_mode_{LimitMode::CLAMP};
}; };
template<typename... Ts> class LightIsOnCondition : public Condition<Ts...> { template<typename... Ts> class LightIsOnCondition : public Condition<Ts...> {

View file

@ -19,10 +19,15 @@ from esphome.const import (
CONF_WARM_WHITE, CONF_WARM_WHITE,
CONF_RANGE_FROM, CONF_RANGE_FROM,
CONF_RANGE_TO, CONF_RANGE_TO,
CONF_BRIGHTNESS_LIMITS,
CONF_LIMIT_MODE,
CONF_MIN_BRIGHTNESS,
CONF_MAX_BRIGHTNESS,
) )
from .types import ( from .types import (
ColorMode, ColorMode,
COLOR_MODES, COLOR_MODES,
LIMIT_MODES,
DimRelativeAction, DimRelativeAction,
ToggleAction, ToggleAction,
LightState, LightState,
@ -167,6 +172,15 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(
cv.positive_time_period_milliseconds cv.positive_time_period_milliseconds
), ),
cv.Optional(CONF_BRIGHTNESS_LIMITS): cv.Schema(
{
cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage,
cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage,
cv.Optional(CONF_LIMIT_MODE, default="CLAMP"): cv.enum(
LIMIT_MODES, upper=True, space="_"
),
}
),
} }
) )
@ -182,6 +196,13 @@ async def light_dim_relative_to_code(config, action_id, template_arg, args):
if CONF_TRANSITION_LENGTH in config: if CONF_TRANSITION_LENGTH in config:
templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
cg.add(var.set_transition_length(templ)) cg.add(var.set_transition_length(templ))
if conf := config.get(CONF_BRIGHTNESS_LIMITS):
cg.add(
var.set_min_max_brightness(
conf[CONF_MIN_BRIGHTNESS], conf[CONF_MAX_BRIGHTNESS]
)
)
cg.add(var.set_limit_mode(conf[CONF_LIMIT_MODE]))
return var return var

View file

@ -25,7 +25,7 @@ class PulseLightEffect : public LightEffect {
return; return;
} }
auto call = this->state_->turn_on(); auto call = this->state_->turn_on();
float out = this->on_ ? this->max_brightness : this->min_brightness; float out = this->on_ ? this->max_brightness_ : this->min_brightness_;
call.set_brightness_if_supported(out); call.set_brightness_if_supported(out);
call.set_transition_length_if_supported(this->on_ ? this->transition_on_length_ : this->transition_off_length_); call.set_transition_length_if_supported(this->on_ ? this->transition_on_length_ : this->transition_off_length_);
this->on_ = !this->on_; this->on_ = !this->on_;
@ -43,8 +43,8 @@ class PulseLightEffect : public LightEffect {
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_min_max_brightness(float min, float max) { void set_min_max_brightness(float min, float max) {
this->min_brightness = min; this->min_brightness_ = min;
this->max_brightness = max; this->max_brightness_ = max;
} }
protected: protected:
@ -53,8 +53,8 @@ class PulseLightEffect : public LightEffect {
uint32_t transition_on_length_{}; uint32_t transition_on_length_{};
uint32_t transition_off_length_{}; uint32_t transition_off_length_{};
uint32_t update_interval_{}; uint32_t update_interval_{};
float min_brightness{0.0}; float min_brightness_{0.0};
float max_brightness{1.0}; float max_brightness_{1.0};
}; };
/// Random effect. Sets random colors every 10 seconds and slowly transitions between them. /// Random effect. Sets random colors every 10 seconds and slowly transitions between them.

View file

@ -41,29 +41,29 @@ class ESPColorCorrection {
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
return 0; return 0;
uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL;
uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
return res; return (uint8_t) std::min(res, uint16_t(255));
} }
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
return 0; return 0;
uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL;
uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
return res; return (uint8_t) std::min(res, uint16_t(255));
} }
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
return 0; return 0;
uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL;
uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
return res; return (uint8_t) std::min(res, uint16_t(255));
} }
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
return 0; return 0;
uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL;
uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
return res; return (uint8_t) std::min(res, uint16_t(255));
} }
protected: protected:

View file

@ -26,6 +26,13 @@ COLOR_MODES = {
"RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE,
} }
# Limit modes
LimitMode = light_ns.enum("LimitMode", is_class=True)
LIMIT_MODES = {
"CLAMP": LimitMode.CLAMP,
"DO_NOTHING": LimitMode.DO_NOTHING,
}
# Actions # Actions
ToggleAction = light_ns.class_("ToggleAction", automation.Action) ToggleAction = light_ns.class_("ToggleAction", automation.Action)
LightControlAction = light_ns.class_("LightControlAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action)

View file

@ -23,7 +23,7 @@ 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, update_to_code
from .defines import CONF_SKIP from .defines import CONF_SKIP
from .encoders import ENCODERS_CONFIG, encoders_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
from .schemas import ( from .schemas import (
@ -266,12 +266,16 @@ 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)
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)
await build_automation(idle_trigger, [], conf) await build_automation(idle_trigger, [], conf)
await initial_focus_to_code(config)
for comp in helpers.lvgl_components_required: for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}") CORE.add_define(f"USE_LVGL_{comp.upper()}")

View file

@ -5,6 +5,7 @@ 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_ID, CONF_TIMEOUT
from esphome.cpp_generator import RawExpression
from esphome.cpp_types import nullptr from esphome.cpp_types import nullptr
from .defines import ( from .defines import (
@ -17,6 +18,7 @@ from .defines import (
from .lv_validation import lv_bool, lv_color, lv_image from .lv_validation import lv_bool, lv_color, lv_image
from .lvcode import ( from .lvcode import (
LVGL_COMP_ARG, LVGL_COMP_ARG,
UPDATE_EVENT,
LambdaContext, LambdaContext,
LocalVariable, LocalVariable,
LvConditional, LvConditional,
@ -25,12 +27,12 @@ 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,
) )
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 (
LV_EVENT,
LV_STATE, LV_STATE,
LvglAction, LvglAction,
LvglCondition, LvglCondition,
@ -38,7 +40,13 @@ from .types import (
lv_disp_t, lv_disp_t,
lv_obj_t, lv_obj_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,
)
async def action_to_code( async def action_to_code(
@ -48,10 +56,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
@ -64,7 +74,7 @@ async def update_to_code(config, action_id, template_arg, args):
widget.type.w_type.value_property is not None widget.type.w_type.value_property is not None
and widget.type.w_type.value_property in config and widget.type.w_type.value_property in config
): ):
lv.event_send(widget.obj, LV_EVENT.VALUE_CHANGED, nullptr) lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
widgets = await get_widgets(config[CONF_ID]) widgets = await get_widgets(config[CONF_ID])
return await action_to_code(widgets, do_update, action_id, template_arg, args) return await action_to_code(widgets, do_update, action_id, template_arg, args)
@ -147,7 +157,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)

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
""" """
@ -413,6 +414,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align"
CONF_GRID_ROWS = "grid_rows" CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode" CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home" CONF_HOME = "home"
CONF_INITIAL_FOCUS = "initial_focus"
CONF_KEY_CODE = "key_code" CONF_KEY_CODE = "key_code"
CONF_LAYOUT = "layout" CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button" CONF_LEFT_BUTTON = "left_button"
@ -430,6 +432,8 @@ CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select" CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked" 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"
@ -461,6 +465,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"
@ -469,6 +474,7 @@ CONF_TOP_LAYER = "top_layer"
CONF_TOUCHSCREENS = "touchscreens" CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSPARENCY_KEY = "transparency_key" CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme" CONF_THEME = "theme"
CONF_UPDATE_ON_RELEASE = "update_on_release"
CONF_VISIBLE_ROW_COUNT = "visible_row_count" CONF_VISIBLE_ROW_COUNT = "visible_row_count"
CONF_WIDGET = "widget" CONF_WIDGET = "widget"
CONF_WIDGETS = "widgets" CONF_WIDGETS = "widgets"
@ -499,4 +505,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,6 +8,7 @@ from .defines import (
CONF_DEFAULT_GROUP, CONF_DEFAULT_GROUP,
CONF_ENCODERS, CONF_ENCODERS,
CONF_ENTER_BUTTON, CONF_ENTER_BUTTON,
CONF_INITIAL_FOCUS,
CONF_LEFT_BUTTON, CONF_LEFT_BUTTON,
CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME, CONF_LONG_PRESS_TIME,
@ -67,3 +68,10 @@ async def encoders_to_code(var, config):
else: else:
group = default_group group = default_group
lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group)
async def initial_focus_to_code(config):
for enc_conf in config[CONF_ENCODERS]:
if default_focus := enc_conf.get(CONF_INITIAL_FOCUS):
obj = await cg.get_variable(default_focus)
lv.group_focus_obj(obj)

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

@ -38,7 +38,7 @@ class LVLight : public light::LightOutput {
void set_value_(lv_color_t value) { void set_value_(lv_color_t value) {
lv_led_set_color(this->obj_, value); lv_led_set_color(this->obj_, value);
lv_led_on(this->obj_); lv_led_on(this->obj_);
lv_event_send(this->obj_, lv_custom_event, nullptr); lv_event_send(this->obj_, lv_api_event, nullptr);
} }
lv_obj_t *obj_{}; lv_obj_t *obj_{};
optional<lv_color_t> initial_value_{}; optional<lv_color_t> initial_value_{};

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

@ -29,7 +29,11 @@ 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, "ev")]
CUSTOM_EVENT = literal("lvgl::lv_custom_event") # 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.
# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction.
API_EVENT = literal("lvgl::lv_api_event")
UPDATE_EVENT = literal("lvgl::lv_update_event")
def get_line_marks(value) -> list: def get_line_marks(value) -> list:
@ -172,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)
@ -179,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)

View file

@ -27,7 +27,8 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
area->y2++; area->y2++;
} }
lv_event_code_t lv_custom_event; // NOLINT lv_event_code_t lv_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); }
void LvglComponent::set_paused(bool paused, bool show_snow) { void LvglComponent::set_paused(bool paused, bool show_snow) {
this->paused_ = paused; this->paused_ = paused;
@ -40,15 +41,18 @@ void LvglComponent::set_paused(bool paused, bool show_snow) {
} }
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
lv_obj_add_event_cb(obj, callback, event, this); lv_obj_add_event_cb(obj, callback, event, this);
if (event == LV_EVENT_VALUE_CHANGED) {
lv_obj_add_event_cb(obj, callback, lv_custom_event, this);
}
} }
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2) { lv_event_code_t event2) {
this->add_event_cb(obj, callback, event1); this->add_event_cb(obj, callback, event1);
this->add_event_cb(obj, callback, event2); this->add_event_cb(obj, callback, event2);
} }
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2, lv_event_code_t event3) {
this->add_event_cb(obj, callback, event1);
this->add_event_cb(obj, callback, event2);
this->add_event_cb(obj, callback, event3);
}
void LvglComponent::add_page(LvPageType *page) { void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page); this->pages_.push_back(page);
page->setup(this->pages_.size() - 1); page->setup(this->pages_.size() - 1);
@ -228,7 +232,8 @@ void LvglComponent::setup() {
lv_log_register_print_cb(log_cb); lv_log_register_print_cb(log_cb);
#endif #endif
lv_init(); lv_init();
lv_custom_event = static_cast<lv_event_code_t>(lv_event_register_id()); lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
auto *display = this->displays_[0]; auto *display = this->displays_[0];
size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
@ -289,6 +294,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

@ -38,7 +38,9 @@
namespace esphome { namespace esphome {
namespace lvgl { namespace lvgl {
extern lv_event_code_t lv_custom_event; // NOLINT extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT
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
@ -133,6 +135,8 @@ class LvglComponent : public PollingComponent {
void set_paused(bool paused, bool show_snow); void set_paused(bool paused, bool show_snow);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
lv_event_code_t event3);
bool is_paused() const { return this->paused_; } bool is_paused() const { return this->paused_; }
void add_page(LvPageType *page); void add_page(LvPageType *page);
void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);

View file

@ -3,12 +3,20 @@ from esphome.components import number
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_ANIMATED, CONF_LVGL_ID, CONF_WIDGET from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
from ..lv_validation import animated from ..lv_validation import animated
from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
lv,
lv_add,
)
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)
@ -19,6 +27,7 @@ CONFIG_SCHEMA = (
{ {
cv.Required(CONF_WIDGET): cv.use_id(LvNumber), cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
cv.Optional(CONF_ANIMATED, default=True): animated, cv.Optional(CONF_ANIMATED, default=True): animated,
cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean,
} }
) )
) )
@ -35,18 +44,25 @@ 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, CUSTOM_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 = (
LV_EVENT.VALUE_CHANGED
if not config[CONF_UPDATE_ON_RELEASE]
else LV_EVENT.RELEASED
)
async with LvContext(paren): async with LvContext(paren):
lv_add(var.set_control_lambda(await control.get_lambda())) lv_add(var.set_control_lambda(await control.get_lambda()))
lv_add( lv_add(
paren.add_event_cb( paren.add_event_cb(
widget.obj, await event.get_lambda(), LV_EVENT.VALUE_CHANGED widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code
) )
) )
lv_add(var.publish_state(widget.get_value())) lv_add(var.publish_state(widget.get_value()))

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <utility>
#include "esphome/components/number/number.h" #include "esphome/components/number/number.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -11,7 +13,7 @@ namespace lvgl {
class LVGLNumber : public number::Number { class LVGLNumber : public number::Number {
public: public:
void set_control_lambda(std::function<void(float)> control_lambda) { void set_control_lambda(std::function<void(float)> control_lambda) {
this->control_lambda_ = control_lambda; this->control_lambda_ = std::move(control_lambda);
if (this->initial_state_.has_value()) { if (this->initial_state_.has_value()) {
this->control_lambda_(this->initial_state_.value()); this->control_lambda_(this->initial_state_.value());
this->initial_state_.reset(); this->initial_state_.reset();
@ -20,10 +22,11 @@ class LVGLNumber : public number::Number {
protected: protected:
void control(float value) override { void control(float value) override {
if (this->control_lambda_ != nullptr) if (this->control_lambda_ != nullptr) {
this->control_lambda_(value); this->control_lambda_(value);
else } else {
this->initial_state_ = value; this->initial_state_ = value;
}
} }
std::function<void(float)> control_lambda_{}; std::function<void(float)> control_lambda_{};
optional<float> initial_state_{}; optional<float> initial_state_{};

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,17 +9,27 @@ 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,
) )
from esphome.core import TimePeriod 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, types as ty 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
from .types import WidgetType, lv_group_t from .types import (
LVEncoderListener,
LvType,
WidgetType,
lv_group_t,
lv_obj_t,
lv_pseudo_button_t,
lv_style_t,
)
# this will be populated later, in __init__.py to avoid circular imports. # this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {} WIDGET_TYPES: dict = {}
@ -38,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),
) )
} }
) )
@ -46,7 +63,7 @@ TEXT_SCHEMA = cv.Schema(
LIST_ACTION_SCHEMA = cv.ensure_list( LIST_ACTION_SCHEMA = cv.ensure_list(
cv.maybe_simple_value( cv.maybe_simple_value(
{ {
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), cv.Required(CONF_ID): cv.use_id(lv_pseudo_button_t),
}, },
key=CONF_ID, key=CONF_ID,
) )
@ -59,9 +76,10 @@ PRESS_TIME = cv.All(
ENCODER_SCHEMA = cv.Schema( ENCODER_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.All( cv.GenerateID(): cv.All(
cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") cv.declare_id(LVEncoderListener), requires_component("binary_sensor")
), ),
cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t),
cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
} }
@ -94,6 +112,7 @@ STYLE_PROPS = {
).several_of, ).several_of,
"border_width": cv.positive_int, "border_width": cv.positive_int,
"clip_corner": lvalid.lv_bool, "clip_corner": lvalid.lv_bool,
"color_filter_opa": lvalid.opacity,
"height": lvalid.size, "height": lvalid.size,
"image_recolor": lvalid.lv_color, "image_recolor": lvalid.lv_color,
"image_recolor_opa": lvalid.opacity, "image_recolor_opa": lvalid.opacity,
@ -106,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_,
@ -161,7 +178,7 @@ STYLE_REMAP = {
# Complete object style schema # Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{ {
cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)), cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(lv_style_t)),
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of, ).one_of,
@ -193,12 +210,12 @@ def part_schema(widget_type: WidgetType):
) )
def automation_schema(typ: ty.LvType): def automation_schema(typ: LvType):
if typ.has_on_value: if typ.has_on_value:
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, ty.LvType): if isinstance(typ, LvType):
template = Trigger.template(typ.get_arg_type()) template = Trigger.template(typ.get_arg_type())
else: else:
template = Trigger.template() template = Trigger.template()
@ -261,7 +278,7 @@ LAYOUT_SCHEMAS = {}
ALIGN_TO_SCHEMA = { ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.Schema( cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
{ {
cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t), cv.Required(CONF_ID): cv.use_id(lv_obj_t),
cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of,
cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent,
cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent,
@ -294,6 +311,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(
@ -302,6 +321,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,
@ -328,7 +349,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

@ -4,10 +4,18 @@ import esphome.config_validation as cv
from esphome.const import CONF_OPTIONS from esphome.const import CONF_OPTIONS
from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
lv,
lv_add,
)
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)
@ -29,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, CUSTOM_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(
@ -41,6 +51,7 @@ async def to_code(config):
widget.obj, widget.obj,
await pub_ctx.get_lambda(), await pub_ctx.get_lambda(),
LV_EVENT.VALUE_CHANGED, LV_EVENT.VALUE_CHANGED,
UPDATE_EVENT,
) )
) )
lv_add(selector.publish_index(widget.get_value())) lv_add(selector.publish_index(widget.get_value()))

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <utility>
#include "esphome/components/select/select.h" #include "esphome/components/select/select.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -28,7 +30,7 @@ static std::vector<std::string> split_string(const std::string &str) {
class LVGLSelect : public select::Select { class LVGLSelect : public select::Select {
public: public:
void set_control_lambda(std::function<void(size_t)> lambda) { void set_control_lambda(std::function<void(size_t)> lambda) {
this->control_lambda_ = lambda; this->control_lambda_ = std::move(lambda);
if (this->initial_state_.has_value()) { if (this->initial_state_.has_value()) {
this->control(this->initial_state_.value()); this->control(this->initial_state_.value());
this->initial_state_.reset(); this->initial_state_.reset();

View file

@ -3,10 +3,18 @@ from esphome.components.sensor import Sensor, new_sensor, sensor_schema
import esphome.config_validation as cv import esphome.config_validation as cv
from ..defines import CONF_LVGL_ID, CONF_WIDGET from ..defines import CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import EVENT_ARG, LVGL_COMP_ARG, LambdaContext, LvContext, lv_add from ..lvcode import (
API_EVENT,
EVENT_ARG,
LVGL_COMP_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
lv_add,
)
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)
@ -25,11 +33,16 @@ 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):
lv_add( lv_add(
paren.add_event_cb( paren.add_event_cb(
widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED widget.obj,
await lamb.get_lambda(),
LV_EVENT.VALUE_CHANGED,
API_EVENT,
UPDATE_EVENT,
) )
) )

View file

@ -3,10 +3,11 @@ 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 (
CUSTOM_EVENT, API_EVENT,
EVENT_ARG, EVENT_ARG,
UPDATE_EVENT,
LambdaContext, LambdaContext,
LvConditional, LvConditional,
LvContext, LvContext,
@ -15,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 = (
@ -34,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:
@ -41,7 +43,8 @@ async def to_code(config):
widget.add_state(LV_STATE.CHECKED) widget.add_state(LV_STATE.CHECKED)
cond.else_() cond.else_()
widget.clear_state(LV_STATE.CHECKED) widget.clear_state(LV_STATE.CHECKED)
lv.event_send(widget.obj, CUSTOM_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(
@ -49,6 +52,7 @@ async def to_code(config):
widget.obj, widget.obj,
await checked_ctx.get_lambda(), await checked_ctx.get_lambda(),
LV_EVENT.VALUE_CHANGED, LV_EVENT.VALUE_CHANGED,
UPDATE_EVENT,
) )
) )
lv_add(switch.publish_state(widget.get_value())) lv_add(switch.publish_state(widget.get_value()))

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <utility>
#include "esphome/components/switch/switch.h" #include "esphome/components/switch/switch.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -11,7 +13,7 @@ namespace lvgl {
class LVGLSwitch : public switch_::Switch { class LVGLSwitch : public switch_::Switch {
public: public:
void set_control_lambda(std::function<void(bool)> state_lambda) { void set_control_lambda(std::function<void(bool)> state_lambda) {
this->state_lambda_ = state_lambda; this->state_lambda_ = std::move(state_lambda);
if (this->initial_state_.has_value()) { if (this->initial_state_.has_value()) {
this->state_lambda_(this->initial_state_.value()); this->state_lambda_(this->initial_state_.value());
this->initial_state_.reset(); this->initial_state_.reset();
@ -20,10 +22,11 @@ class LVGLSwitch : public switch_::Switch {
protected: protected:
void write_state(bool value) override { void write_state(bool value) override {
if (this->state_lambda_ != nullptr) if (this->state_lambda_ != nullptr) {
this->state_lambda_(value); this->state_lambda_(value);
else } else {
this->initial_state_ = value; this->initial_state_ = value;
}
} }
std::function<void(bool)> state_lambda_{}; std::function<void(bool)> state_lambda_{};
optional<bool> initial_state_{}; optional<bool> initial_state_{};

View file

@ -4,10 +4,18 @@ from esphome.components.text import new_text
import esphome.config_validation as cv import esphome.config_validation as cv
from ..defines import CONF_LVGL_ID, CONF_WIDGET from ..defines import CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
lv,
lv_add,
)
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)
@ -24,16 +32,21 @@ 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, CUSTOM_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):
widget.var.set_control_lambda(await control.get_lambda()) widget.var.set_control_lambda(await control.get_lambda())
lv_add( lv_add(
paren.add_event_cb( paren.add_event_cb(
widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED widget.obj,
await lamb.get_lambda(),
LV_EVENT.VALUE_CHANGED,
UPDATE_EVENT,
) )
) )
lv_add(textvar.publish_state(widget.get_value())) lv_add(textvar.publish_state(widget.get_value()))

View file

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <utility>
#include "esphome/components/text/text.h" #include "esphome/components/text/text.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -11,7 +13,7 @@ namespace lvgl {
class LVGLText : public text::Text { class LVGLText : public text::Text {
public: public:
void set_control_lambda(std::function<void(const std::string)> control_lambda) { void set_control_lambda(std::function<void(const std::string)> control_lambda) {
this->control_lambda_ = control_lambda; this->control_lambda_ = std::move(control_lambda);
if (this->initial_state_.has_value()) { if (this->initial_state_.has_value()) {
this->control_lambda_(this->initial_state_.value()); this->control_lambda_(this->initial_state_.value());
this->initial_state_.reset(); this->initial_state_.reset();
@ -20,10 +22,11 @@ class LVGLText : public text::Text {
protected: protected:
void control(const std::string &value) override { void control(const std::string &value) override {
if (this->control_lambda_ != nullptr) if (this->control_lambda_ != nullptr) {
this->control_lambda_(value); this->control_lambda_(value);
else } else {
this->initial_state_ = value; this->initial_state_ = value;
}
} }
std::function<void(const std::string)> control_lambda_{}; std::function<void(const std::string)> control_lambda_{};
optional<std::string> initial_state_{}; optional<std::string> initial_state_{};

View file

@ -7,10 +7,10 @@ from esphome.components.text_sensor import (
import esphome.config_validation as cv import esphome.config_validation as cv
from ..defines import CONF_LVGL_ID, CONF_WIDGET from ..defines import CONF_LVGL_ID, CONF_WIDGET
from ..lvcode import EVENT_ARG, 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:
@ -36,5 +37,7 @@ async def to_code(config):
widget.obj, widget.obj,
await pressed_ctx.get_lambda(), await pressed_ctx.get_lambda(),
LV_EVENT.VALUE_CHANGED, LV_EVENT.VALUE_CHANGED,
API_EVENT,
UPDATE_EVENT,
) )
) )

View file

@ -11,7 +11,15 @@ from .defines import (
LV_EVENT_TRIGGERS, LV_EVENT_TRIGGERS,
literal, literal,
) )
from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add from .lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvConditional,
lv,
lv_add,
)
from .types import LV_EVENT from .types import LV_EVENT
from .widgets import widget_map from .widgets import widget_map
@ -34,9 +42,16 @@ async def generate_triggers(lv_component):
conf = conf[0] conf = conf[0]
w.add_flag("LV_OBJ_FLAG_CLICKABLE") w.add_flag("LV_OBJ_FLAG_CLICKABLE")
event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()])
await add_trigger(conf, event, lv_component, w) await add_trigger(conf, lv_component, w, event)
for conf in w.config.get(CONF_ON_VALUE, ()): for conf in w.config.get(CONF_ON_VALUE, ()):
await add_trigger(conf, LV_EVENT.VALUE_CHANGED, lv_component, w) await add_trigger(
conf,
lv_component,
w,
LV_EVENT.VALUE_CHANGED,
API_EVENT,
UPDATE_EVENT,
)
# Generate align to directives while we're here # Generate align to directives while we're here
if align_to := w.config.get(CONF_ALIGN_TO): if align_to := w.config.get(CONF_ALIGN_TO):
@ -47,7 +62,7 @@ async def generate_triggers(lv_component):
lv.obj_align_to(w.obj, target, align, x, y) lv.obj_align_to(w.obj, target, align, x, y)
async def add_trigger(conf, event, lv_component, w): 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()
@ -56,4 +71,4 @@ async def add_trigger(conf, event, lv_component, w):
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))
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), event)) lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events))

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"),
@ -220,6 +231,19 @@ async def get_widget_(wid: Widget):
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 {
@ -316,8 +351,13 @@ async def set_obj_properties(w: Widget, config):
flag_clr = set() flag_clr = set()
flag_set = 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 +367,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 +395,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

@ -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

@ -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 {

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