Merge pull request #3567 from esphome/bump-2022.6.0

2022.6.0
This commit is contained in:
Jesse Hills 2022-06-16 12:59:26 +12:00 committed by GitHub
commit 8c9948bb56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 3559 additions and 1699 deletions

View File

@ -88,6 +88,7 @@ esphome/components/honeywellabp/* @RubyBailey
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/i2c/* @esphome/core
esphome/components/i2s_audio/* @jesserockz
esphome/components/improv_serial/* @esphome/core
esphome/components/ina260/* @MrEditor97
esphome/components/inkbird_ibsth1_mini/* @fkirill
@ -102,6 +103,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny
esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger
esphome/components/max7219digit/* @rspaargaren
esphome/components/max9611/* @mckaymatthew
@ -119,6 +121,7 @@ esphome/components/mcp47a1/* @jesserockz
esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/mitsubishi/* @RubyBailey
@ -178,6 +181,7 @@ esphome/components/sen5x/* @martgras
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @SenexCrenshaw @martgras
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet
@ -222,6 +226,7 @@ esphome/components/tsl2591/* @wjcarpenter
esphome/components/tuya/binary_sensor/* @jesserockz
esphome/components/tuya/climate/* @jesserockz
esphome/components/tuya/number/* @frankiboy1
esphome/components/tuya/select/* @bearpawmaxim
esphome/components/tuya/sensor/* @jesserockz
esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra

View File

@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
// calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
// also take into account min_power
auto min_us = this->cycle_time_us * this->min_power / 1000;
this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
// calculate required value to provide a true RMS voltage output
this->enable_time_us =
std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) *
(this->cycle_time_us - min_us)) /
65535);
if (this->method == DIM_METHOD_LEADING_PULSE) {
// Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
// this is for brightness near 99%

View File

@ -15,10 +15,21 @@ namespace esphome {
namespace adc {
static const char *const TAG = "adc";
// 13 bits for S3 / 12 bit for all other esp32 variants
// create a const to avoid the repated cast to enum
// 13bit for S2, and 12bit for all other esp32 variants
#ifdef USE_ESP32
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
#if USE_ESP32_VARIANT_ESP32S2
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13;
#else
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
#endif
#endif
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
#endif
void ADCSensor::setup() {
@ -75,16 +86,16 @@ void ADCSensor::dump_config() {
} else {
switch (this->attenuation_) {
case ADC_ATTEN_DB_0:
ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
ESP_LOGCONFIG(TAG, " Attenuation: 0db");
break;
case ADC_ATTEN_DB_2_5:
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)");
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
break;
case ADC_ATTEN_DB_6:
ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)");
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
break;
case ADC_ATTEN_DB_11:
ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)");
ESP_LOGCONFIG(TAG, " Attenuation: 11db");
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
@ -129,16 +140,16 @@ float ADCSensor::sample() {
return mv / 1000.0f;
}
int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095;
int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
raw11 = adc1_get_raw(channel_);
if (raw11 < 4095) {
if (raw11 < ADC_MAX) {
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
raw6 = adc1_get_raw(channel_);
if (raw6 < 4095) {
if (raw6 < ADC_MAX) {
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
raw2 = adc1_get_raw(channel_);
if (raw2 < 4095) {
if (raw2 < ADC_MAX) {
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
raw0 = adc1_get_raw(channel_);
}
@ -154,15 +165,15 @@ float ADCSensor::sample() {
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]);
// Contribution of each value, in range 0-2048
uint32_t c11 = std::min(raw11, 2048);
uint32_t c6 = 2048 - std::abs(raw6 - 2048);
uint32_t c2 = 2048 - std::abs(raw2 - 2048);
uint32_t c0 = std::min(4095 - raw0, 2048);
// max theoretical csum value is 2048*4 = 8192
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
uint32_t c11 = std::min(raw11, ADC_HALF);
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
// max theoretical csum value is 4096*4 = 16384
uint32_t csum = c11 + c6 + c2 + c0;
// each mv is max 3900; so max value is 3900*2048*4, fits in unsigned
// each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
return mv_scaled / (float) (csum * 1000U);
}

View File

@ -40,6 +40,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom
void setup() override;
void display();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
int get_width_internal() override;
int get_height_internal() override;

View File

@ -42,6 +42,7 @@ service APIConnection {
rpc select_command (SelectCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
}
@ -991,7 +992,7 @@ message ListEntitiesLockResponse {
bool supports_open = 9;
bool requires_code = 10;
# Not yet implemented:
// Not yet implemented:
string code_format = 11;
}
message LockStateResponse {
@ -1010,7 +1011,7 @@ message LockCommandRequest {
fixed32 key = 1;
LockCommand command = 2;
# Not yet implemented:
// Not yet implemented:
bool has_code = 3;
string code = 4;
}
@ -1040,3 +1041,60 @@ message ButtonCommandRequest {
fixed32 key = 1;
}
// ==================== MEDIA PLAYER ====================
enum MediaPlayerState {
MEDIA_PLAYER_STATE_NONE = 0;
MEDIA_PLAYER_STATE_IDLE = 1;
MEDIA_PLAYER_STATE_PLAYING = 2;
MEDIA_PLAYER_STATE_PAUSED = 3;
}
enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_PLAY = 0;
MEDIA_PLAYER_COMMAND_PAUSE = 1;
MEDIA_PLAYER_COMMAND_STOP = 2;
MEDIA_PLAYER_COMMAND_MUTE = 3;
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
bool supports_pause = 8;
}
message MediaPlayerStateResponse {
option (id) = 64;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
fixed32 key = 1;
MediaPlayerState state = 2;
float volume = 3;
bool muted = 4;
}
message MediaPlayerCommandRequest {
option (id) = 65;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
fixed32 key = 1;
bool has_command = 2;
MediaPlayerCommand command = 3;
bool has_volume = 4;
float volume = 5;
bool has_media_url = 6;
string media_url = 7;
}

View File

@ -12,9 +12,6 @@
#ifdef USE_HOMEASSISTANT_TIME
#include "esphome/components/homeassistant/time/homeassistant_time.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan_helpers.h"
#endif
namespace esphome {
namespace api {
@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#endif
#ifdef USE_FAN
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
bool APIConnection::send_fan_state(fan::Fan *fan) {
if (!this->state_subscription_)
return false;
@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
resp.oscillating = fan->oscillating;
if (traits.supports_speed()) {
resp.speed_level = fan->speed;
resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count()));
}
if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(fan->direction);
@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (fan == nullptr)
return;
auto traits = fan->get_traits();
auto call = fan->make_call();
if (msg.has_state)
call.set_state(msg.state);
@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_speed_level) {
// Prefer level
call.set_speed(msg.speed_level);
} else if (msg.has_speed) {
call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count()));
}
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
call.perform();
}
#pragma GCC diagnostic pop
#endif
#ifdef USE_LIGHT
@ -745,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
if (!this->state_subscription_)
return false;
MediaPlayerStateResponse resp{};
resp.key = media_player->get_object_id_hash();
resp.state = static_cast<enums::MediaPlayerState>(media_player->state);
resp.volume = media_player->volume;
resp.muted = media_player->is_muted();
return this->send_media_player_state_response(resp);
}
bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
ListEntitiesMediaPlayerResponse msg;
msg.key = media_player->get_object_id_hash();
msg.object_id = media_player->get_object_id();
msg.name = media_player->get_name();
msg.unique_id = get_default_unique_id("media_player", media_player);
msg.icon = media_player->get_icon();
msg.disabled_by_default = media_player->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(media_player->get_entity_category());
auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause();
return this->send_list_entities_media_player_response(msg);
}
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
if (media_player == nullptr)
return;
auto call = media_player->make_call();
if (msg.has_command) {
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
}
if (msg.has_volume) {
call.set_volume(msg.volume);
}
if (msg.has_media_url) {
call.set_media_url(msg.media_url);
}
call.perform();
}
#endif
#ifdef USE_ESP32_CAMERA
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
if (!this->state_subscription_)

View File

@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection {
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
bool send_lock_info(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
bool send_media_player_info(media_player::MediaPlayer *media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif
bool send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {

View File

@ -308,6 +308,36 @@ template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockComma
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) {
switch (value) {
case enums::MEDIA_PLAYER_STATE_NONE:
return "MEDIA_PLAYER_STATE_NONE";
case enums::MEDIA_PLAYER_STATE_IDLE:
return "MEDIA_PLAYER_STATE_IDLE";
case enums::MEDIA_PLAYER_STATE_PLAYING:
return "MEDIA_PLAYER_STATE_PLAYING";
case enums::MEDIA_PLAYER_STATE_PAUSED:
return "MEDIA_PLAYER_STATE_PAUSED";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) {
switch (value) {
case enums::MEDIA_PLAYER_COMMAND_PLAY:
return "MEDIA_PLAYER_COMMAND_PLAY";
case enums::MEDIA_PLAYER_COMMAND_PAUSE:
return "MEDIA_PLAYER_COMMAND_PAUSE";
case enums::MEDIA_PLAYER_COMMAND_STOP:
return "MEDIA_PLAYER_COMMAND_STOP";
case enums::MEDIA_PLAYER_COMMAND_MUTE:
return "MEDIA_PLAYER_COMMAND_MUTE";
case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
return "MEDIA_PLAYER_COMMAND_UNMUTE";
default:
return "UNKNOWN";
}
}
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
@ -4574,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->supports_pause = value.as_bool();
return true;
}
default:
return false;
}
}
bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_bool(8, this->supports_pause);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesMediaPlayerResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" supports_pause: ");
out.append(YESNO(this->supports_pause));
out.append("\n");
out.append("}");
}
#endif
bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->state = value.as_enum<enums::MediaPlayerState>();
return true;
}
case 4: {
this->muted = value.as_bool();
return true;
}
default:
return false;
}
}
bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 3: {
this->volume = value.as_float();
return true;
}
default:
return false;
}
}
void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_enum<enums::MediaPlayerState>(2, this->state);
buffer.encode_float(3, this->volume);
buffer.encode_bool(4, this->muted);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("MediaPlayerStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" state: ");
out.append(proto_enum_to_string<enums::MediaPlayerState>(this->state));
out.append("\n");
out.append(" volume: ");
sprintf(buffer, "%g", this->volume);
out.append(buffer);
out.append("\n");
out.append(" muted: ");
out.append(YESNO(this->muted));
out.append("\n");
out.append("}");
}
#endif
bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->has_command = value.as_bool();
return true;
}
case 3: {
this->command = value.as_enum<enums::MediaPlayerCommand>();
return true;
}
case 4: {
this->has_volume = value.as_bool();
return true;
}
case 6: {
this->has_media_url = value.as_bool();
return true;
}
default:
return false;
}
}
bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 7: {
this->media_url = value.as_string();
return true;
}
default:
return false;
}
}
bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 5: {
this->volume = value.as_float();
return true;
}
default:
return false;
}
}
void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->has_command);
buffer.encode_enum<enums::MediaPlayerCommand>(3, this->command);
buffer.encode_bool(4, this->has_volume);
buffer.encode_float(5, this->volume);
buffer.encode_bool(6, this->has_media_url);
buffer.encode_string(7, this->media_url);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("MediaPlayerCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%u", this->key);
out.append(buffer);
out.append("\n");
out.append(" has_command: ");
out.append(YESNO(this->has_command));
out.append("\n");
out.append(" command: ");
out.append(proto_enum_to_string<enums::MediaPlayerCommand>(this->command));
out.append("\n");
out.append(" has_volume: ");
out.append(YESNO(this->has_volume));
out.append("\n");
out.append(" volume: ");
sprintf(buffer, "%g", this->volume);
out.append(buffer);
out.append("\n");
out.append(" has_media_url: ");
out.append(YESNO(this->has_media_url));
out.append("\n");
out.append(" media_url: ");
out.append("'").append(this->media_url).append("'");
out.append("\n");
out.append("}");
}
#endif
} // namespace api
} // namespace esphome

View File

@ -141,6 +141,19 @@ enum LockCommand : uint32_t {
LOCK_LOCK = 1,
LOCK_OPEN = 2,
};
enum MediaPlayerState : uint32_t {
MEDIA_PLAYER_STATE_NONE = 0,
MEDIA_PLAYER_STATE_IDLE = 1,
MEDIA_PLAYER_STATE_PLAYING = 2,
MEDIA_PLAYER_STATE_PAUSED = 3,
};
enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_PLAY = 0,
MEDIA_PLAYER_COMMAND_PAUSE = 1,
MEDIA_PLAYER_COMMAND_STOP = 2,
MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
};
} // namespace enums
@ -1146,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class ListEntitiesMediaPlayerResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool supports_pause{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class MediaPlayerStateResponse : public ProtoMessage {
public:
uint32_t key{0};
enums::MediaPlayerState state{};
float volume{0.0f};
bool muted{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class MediaPlayerCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
bool has_command{false};
enums::MediaPlayerCommand command{};
bool has_volume{false};
float volume{0.0f};
bool has_media_url{false};
std::string media_url{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
} // namespace api
} // namespace esphome

View File

@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit
#endif
#ifdef USE_BUTTON
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesMediaPlayerResponse>(msg, 63);
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<MediaPlayerStateResponse>(msg, 64);
}
#endif
#ifdef USE_MEDIA_PLAYER
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) {
case 1: {
@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
#endif
this->on_button_command_request(msg);
#endif
break;
}
case 65: {
#ifdef USE_MEDIA_PLAYER
MediaPlayerCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str());
#endif
this->on_media_player_command_request(msg);
#endif
break;
}
@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg)
this->lock_command(msg);
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->media_player_command(msg);
}
#endif
} // namespace api
} // namespace esphome

View File

@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_BUTTON
virtual void on_button_command_request(const ButtonCommandRequest &value){};
#endif
#ifdef USE_MEDIA_PLAYER
bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg);
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state_response(const MediaPlayerStateResponse &msg);
#endif
#ifdef USE_MEDIA_PLAYER
virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif
};
} // namespace api

View File

@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) {
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_media_player_state(obj);
}
#endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -68,6 +68,9 @@ class APIServer : public Component, public Controller {
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

View File

@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
#endif
#ifdef USE_MEDIA_PLAYER
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
return this->client_->send_media_player_info(media_player);
}
#endif
} // namespace api
} // namespace esphome

View File

@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
#endif
bool on_end() override;

View File

@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) {
#ifdef USE_LOCK
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); }
#endif
#ifdef USE_MEDIA_PLAYER
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) {
return this->client_->send_media_player_state(media_player);
}
#endif
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api

View File

@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
#endif
protected:
APIConnection *client_;

View File

@ -36,6 +36,14 @@ static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
return -1;
}
static BedjetButton heat_button(BedjetHeatMode mode) {
BedjetButton btn = BTN_HEAT;
if (mode == HEAT_MODE_EXTENDED) {
btn = BTN_EXTHT;
}
return btn;
}
void Bedjet::upgrade_firmware() {
auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
auto status = this->write_bedjet_packet_(pkt);
@ -117,7 +125,7 @@ void Bedjet::control(const ClimateCall &call) {
pkt = this->codec_->get_button_request(BTN_OFF);
break;
case climate::CLIMATE_MODE_HEAT:
pkt = this->codec_->get_button_request(BTN_HEAT);
pkt = this->codec_->get_button_request(heat_button(this->heating_mode_));
break;
case climate::CLIMATE_MODE_FAN_ONLY:
pkt = this->codec_->get_button_request(BTN_COOL);
@ -186,6 +194,8 @@ void Bedjet::control(const ClimateCall &call) {
pkt = this->codec_->get_button_request(BTN_M2);
} else if (preset == "M3") {
pkt = this->codec_->get_button_request(BTN_M3);
} else if (preset == "LTD HT") {
pkt = this->codec_->get_button_request(BTN_HEAT);
} else if (preset == "EXT HT") {
pkt = this->codec_->get_button_request(BTN_EXTHT);
} else {
@ -298,7 +308,7 @@ void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
#ifdef USE_TIME
if (this->time_id_.has_value()) {
this->send_local_time_();
this->send_local_time();
}
#endif
break;
@ -453,40 +463,47 @@ uint8_t Bedjet::write_notify_config_descriptor_(bool enable) {
#ifdef USE_TIME
/** Attempts to sync the local time (via `time_id`) to the BedJet device. */
void Bedjet::send_local_time_() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
auto *time_id = *this->time_id_;
time::ESPTime now = time_id->now();
if (now.is_valid()) {
uint8_t hour = now.hour;
uint8_t minute = now.minute;
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
void Bedjet::send_local_time() {
if (this->time_id_.has_value()) {
auto *time_id = *this->time_id_;
time::ESPTime now = time_id->now();
if (now.is_valid()) {
this->set_clock(now.hour, now.minute);
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
}
} else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
}
}
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
void Bedjet::setup_time_() {
if (this->time_id_.has_value()) {
this->send_local_time_();
this->send_local_time();
auto *time_id = *this->time_id_;
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
time::ESPTime now = time_id->now();
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
} else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
}
}
#endif
/** Attempt to set the BedJet device's clock to the specified time. */
void Bedjet::set_clock(uint8_t hour, uint8_t minute) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
return;
}
BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
auto status = this->write_bedjet_packet_(pkt);
if (status) {
ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
} else {
ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
}
}
/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */
uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
@ -557,11 +574,25 @@ bool Bedjet::update_status_() {
break;
case MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
this->action = climate::CLIMATE_ACTION_HEATING;
this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->set_custom_preset_("LTD HT");
} else {
this->custom_preset.reset();
}
break;
case MODE_EXTHT:
this->mode = climate::CLIMATE_MODE_HEAT;
this->action = climate::CLIMATE_ACTION_HEATING;
this->custom_preset.reset();
this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->custom_preset.reset();
} else {
this->set_custom_preset_("EXT HT");
}
break;
case MODE_COOL:

View File

@ -38,8 +38,12 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
#ifdef USE_TIME
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
void send_local_time();
#endif
void set_clock(uint8_t hour, uint8_t minute);
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
/** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */
void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; }
/** Attempts to check for and apply firmware updates. */
void upgrade_firmware();
@ -74,6 +78,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
"M2",
"M3",
});
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);
@ -85,11 +94,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
#ifdef USE_TIME
void setup_time_();
void send_local_time_();
optional<time::RealTimeClock *> time_id_{};
#endif
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT;
static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;

View File

@ -24,6 +24,14 @@ enum BedjetMode : uint8_t {
MODE_WAIT = 6,
};
/** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */
enum BedjetHeatMode {
/// HVACMode.HEAT is handled using BTN_HEAT (default)
HEAT_MODE_HEAT,
/// HVACMode.HEAT is handled using BTN_EXTHT
HEAT_MODE_EXTENDED,
};
enum BedjetButton : uint8_t {
/// Turn BedJet off
BTN_OFF = 0x1,

View File

@ -2,6 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate, ble_client, time
from esphome.const import (
CONF_HEAT_MODE,
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_TIME_ID,
@ -14,11 +15,19 @@ bedjet_ns = cg.esphome_ns.namespace("bedjet")
Bedjet = bedjet_ns.class_(
"Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
)
BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode")
BEDJET_HEAT_MODES = {
"heat": BedjetHeatMode.HEAT_MODE_HEAT,
"extended": BedjetHeatMode.HEAT_MODE_EXTENDED,
}
CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Bedjet),
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
BEDJET_HEAT_MODES, lower=True
),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(
CONF_RECEIVE_TIMEOUT, default="0s"
@ -35,6 +44,7 @@ async def to_code(config):
await cg.register_component(var, config)
await climate.register_climate(var, config)
await ble_client.register_ble_node(var, config)
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))
if CONF_TIME_ID in config:
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time_id(time_))

View File

@ -69,7 +69,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
}
}
bool BinarySensor::has_state() const { return this->has_state_; }
uint32_t BinarySensor::hash_base() { return 1210250844UL; }
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor

View File

@ -76,8 +76,6 @@ class BinarySensor : public EntityBase {
virtual std::string device_class();
protected:
uint32_t hash_base() override;
CallbackManager<void(bool)> state_callback_{};
optional<std::string> device_class_{}; ///< Stores the override of the device class
Filter *filter_list_{nullptr};

View File

@ -11,8 +11,6 @@ namespace ble_client {
static const char *const TAG = "ble_sensor";
uint32_t BLESensor::hash_base() { return 343459825UL; }
void BLESensor::loop() {}
void BLESensor::dump_config() {

View File

@ -37,7 +37,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
uint16_t handle;
protected:
uint32_t hash_base() override;
float parse_data_(uint8_t *value, uint16_t value_len);
optional<data_to_value_t> data_to_value_func_{};
bool notify_;

View File

@ -14,8 +14,6 @@ static const char *const TAG = "ble_text_sensor";
static const std::string EMPTY = "";
uint32_t BLETextSensor::hash_base() { return 193967603UL; }
void BLETextSensor::loop() {}
void BLETextSensor::dump_config() {

View File

@ -35,7 +35,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
uint16_t handle;
protected:
uint32_t hash_base() override;
bool notify_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;

View File

@ -25,7 +25,7 @@ OVERSAMPLING_OPTIONS = {
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32x": Oversampling.OVERSAMPLING_X32,
"32X": Oversampling.OVERSAMPLING_X32,
}
IIRFilter = bmp3xx_ns.enum("IIRFilter")

View File

@ -15,7 +15,6 @@ void Button::press() {
this->press_callback_.call();
}
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
uint32_t Button::hash_base() { return 1495763804UL; }
void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
std::string Button::get_device_class() { return this->device_class_; }

View File

@ -47,8 +47,6 @@ class Button : public EntityBase {
*/
virtual void press_action() = 0;
uint32_t hash_base() override;
CallbackManager<void()> press_callback_{};
std::string device_class_{};
};

View File

@ -419,7 +419,6 @@ void Climate::publish_state() {
// Save state
this->save_state_();
}
uint32_t Climate::hash_base() { return 3104134496UL; }
ClimateTraits Climate::get_traits() {
auto traits = this->traits();

View File

@ -282,7 +282,6 @@ class Climate : public EntityBase {
*/
void save_state_();
uint32_t hash_base() override;
void dump_traits_(const char *tag);
CallbackManager<void()> state_callback_{};

View File

@ -33,8 +33,6 @@ const char *cover_operation_to_str(CoverOperation op) {
Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {}
uint32_t Cover::hash_base() { return 1727367479UL; }
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
if (strcasecmp(command, "OPEN") == 0) {

View File

@ -177,7 +177,6 @@ class Cover : public EntityBase {
virtual std::string device_class();
optional<CoverRestoreState> restore_state_();
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
optional<std::string> device_class_override_{};

View File

@ -85,6 +85,12 @@ enum ImageType {
IMAGE_TYPE_RGB565 = 4,
};
enum DisplayType {
DISPLAY_TYPE_BINARY = 1,
DISPLAY_TYPE_GRAYSCALE = 2,
DISPLAY_TYPE_COLOR = 3,
};
enum DisplayRotation {
DISPLAY_ROTATION_0_DEGREES = 0,
DISPLAY_ROTATION_90_DEGREES = 90,
@ -361,6 +367,11 @@ class DisplayBuffer {
virtual int get_width_internal() = 0;
DisplayRotation get_rotation() const { return this->rotation_; }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
virtual DisplayType get_display_type() = 0;
protected:
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);

View File

@ -66,6 +66,9 @@ class ColorUtil {
}
return color_return;
}
static inline Color rgb332_to_color(uint8_t rgb332_color) {
return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332);
}
static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
uint16_t red_color, green_color, blue_color;
@ -100,11 +103,57 @@ class ColorUtil {
}
return 0;
}
static uint32_t color_to_grayscale4(Color color) {
uint32_t gs4 = esp_scale8(color.white, 15);
return gs4;
}
/***
* Converts a Color value to an 8bit index using a 24bit 888 palette.
* Uses euclidiean distance to calculate the linear distance between
* two points in an RGB cube, then iterates through the full palette
* returning the closest match.
* @param[in] color The target color.
* @param[in] palette The 256*3 byte RGB palette.
* @return The 8 bit index of the closest color (e.g. for display buffer).
*/
// static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) {
static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) {
uint8_t closest_index = 0;
uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target
// so far
// int8_t(*plt)[][3] = palette;
int16_t tgt_r = color.r;
int16_t tgt_g = color.g;
int16_t tgt_b = color.b;
uint16_t x, y, z;
// Loop through each row of the palette
for (uint16_t i = 0; i < 256; i++) {
// Get the pallet rgb color
int16_t plt_r = (int16_t) palette[i * 3 + 0];
int16_t plt_g = (int16_t) palette[i * 3 + 1];
int16_t plt_b = (int16_t) palette[i * 3 + 2];
// Calculate euclidian distance (linear distance in rgb cube).
x = (uint32_t) std::abs(tgt_r - plt_r);
y = (uint32_t) std::abs(tgt_g - plt_g);
z = (uint32_t) std::abs(tgt_b - plt_b);
uint32_t dist2 = x * x + y * y + z * z;
if (dist2 < minimum_dist2) {
minimum_dist2 = dist2;
closest_index = (uint8_t) i;
}
}
return closest_index;
}
/***
* Converts an 8bit palette index (e.g. from a display buffer) to a color.
* @param[in] index The index to look up.
* @param[in] palette The 256*3 byte RGB palette.
* @return The RGBW Color object looked up by the palette.
*/
static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) {
Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0);
return color;
}
};
} // namespace display
} // namespace esphome

View File

@ -1,5 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome import pins
from esphome.const import (
CONF_FREQUENCY,
@ -12,6 +13,7 @@ from esphome.const import (
CONF_RESOLUTION,
CONF_BRIGHTNESS,
CONF_CONTRAST,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
from esphome.components.esp32 import add_idf_sdkconfig_option
@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
"ESP32CameraStreamStartTrigger",
automation.Trigger.template(),
)
ESP32CameraStreamStopTrigger = esp32_camera_ns.class_(
"ESP32CameraStreamStopTrigger",
automation.Trigger.template(),
)
ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
FRAME_SIZES = {
"160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern"
CONF_MAX_FRAMERATE = "max_framerate"
CONF_IDLE_FRAMERATE = "idle_framerate"
# stream trigger
CONF_ON_STREAM_START = "on_stream_start"
CONF_ON_STREAM_STOP = "on_stream_stop"
camera_range_param = cv.int_range(min=-2, max=2)
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
cv.framerate, cv.Range(min=0, max=1)
),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraStreamStartTrigger
),
}
),
cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraStreamStopTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -238,3 +265,11 @@ async def to_code(config):
if CORE.using_esp_idf:
cg.add_library("espressif/esp32-camera", "1.0.0")
add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True)
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STREAM_STOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
this->new_image_callback_.add(std::move(f));
}
void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); }
void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); }
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
this->stream_start_callback_.add(std::move(callback));
}
void ESP32Camera::add_stream_stop_callback(std::function<void()> &&callback) {
this->stream_stop_callback_.add(std::move(callback));
}
void ESP32Camera::start_stream(CameraRequester requester) {
this->stream_start_callback_.call();
this->stream_requesters_ |= (1U << requester);
}
void ESP32Camera::stop_stream(CameraRequester requester) {
this->stream_stop_callback_.call();
this->stream_requesters_ &= ~(1U << requester);
}
void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
void ESP32Camera::update_camera_parameters() {
sensor_t *s = esp_camera_sensor_get();
@ -310,7 +322,6 @@ void ESP32Camera::update_camera_parameters() {
}
/* ---------------- Internal methods ---------------- */
uint32_t ESP32Camera::hash_base() { return 3010542557UL; }
bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; }
bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; }
void ESP32Camera::framebuffer_task(void *pv) {

View File

@ -2,6 +2,7 @@
#ifdef USE_ESP32
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
@ -145,9 +146,11 @@ class ESP32Camera : public Component, public EntityBase {
void request_image(CameraRequester requester);
void update_camera_parameters();
void add_stream_start_callback(std::function<void()> &&callback);
void add_stream_stop_callback(std::function<void()> &&callback);
protected:
/* internal methods */
uint32_t hash_base() override;
bool has_requested_image_() const;
bool can_return_image_() const;
@ -187,6 +190,8 @@ class ESP32Camera : public Component, public EntityBase {
QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_;
CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_;
CallbackManager<void()> stream_start_callback_{};
CallbackManager<void()> stream_stop_callback_{};
uint32_t last_idle_request_{0};
uint32_t last_update_{0};
@ -195,6 +200,23 @@ class ESP32Camera : public Component, public EntityBase {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32Camera *global_esp32_camera;
class ESP32CameraStreamStartTrigger : public Trigger<> {
public:
explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {
parent->add_stream_start_callback([this]() { this->trigger(); });
}
protected:
};
class ESP32CameraStreamStopTrigger : public Trigger<> {
public:
explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) {
parent->add_stream_stop_callback([this]() { this->trigger(); });
}
protected:
};
} // namespace esp32_camera
} // namespace esphome

View File

@ -1,5 +1,4 @@
#include "fan.h"
#include "fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@ -61,22 +60,6 @@ void FanCall::validate_() {
}
}
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanCall &FanCall::set_speed(const char *legacy_speed) {
const auto supported_speed_count = this->parent_.get_traits().supported_speed_count();
if (strcasecmp(legacy_speed, "low") == 0) {
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count));
} else if (strcasecmp(legacy_speed, "medium") == 0) {
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count));
} else if (strcasecmp(legacy_speed, "high") == 0) {
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count));
}
return *this;
}
#pragma GCC diagnostic pop
FanCall FanRestoreState::to_call(Fan &fan) {
auto call = fan.make_call();
call.set_state(this->state);
@ -169,7 +152,6 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
if (this->get_traits().supports_direction())
ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
}
uint32_t Fan::hash_base() { return 418001110UL; }
} // namespace fan
} // namespace esphome

View File

@ -16,13 +16,6 @@ namespace fan {
(obj)->dump_traits_(TAG, prefix); \
}
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
FAN_SPEED_LOW = 0, ///< The fan is running on low speed.
FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed.
FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed.
};
/// Simple enum to represent the direction of a fan.
enum class FanDirection { FORWARD = 0, REVERSE = 1 };
@ -143,7 +136,6 @@ class Fan : public EntityBase {
void save_state_();
void dump_traits_(const char *tag, const char *prefix);
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;

View File

@ -1,23 +0,0 @@
#include <cassert>
#include "fan_helpers.h"
namespace esphome {
namespace fan {
// This whole file is deprecated, don't warn about usage of deprecated types in here.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);
return static_cast<FanSpeed>(legacy_level - 1);
}
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) {
const auto enum_level = static_cast<int>(speed) + 1;
const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels);
return static_cast<int>(speed_level);
}
} // namespace fan
} // namespace esphome

View File

@ -1,20 +0,0 @@
#pragma once
#include "fan.h"
namespace esphome {
namespace fan {
// Shut-up about usage of deprecated FanSpeed for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
#pragma GCC diagnostic pop
} // namespace fan
} // namespace esphome

View File

@ -1,5 +1,4 @@
#include "hbridge_fan.h"
#include "esphome/components/fan/fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {

View File

@ -195,7 +195,7 @@ void HydreonRGxxComponent::process_line_() {
if (n == std::string::npos) {
continue;
}
int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr);
this->sensors_[i]->publish_state(data);
ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
this->sensors_received_ |= (1 << i);

View File

@ -37,7 +37,7 @@ SUPPORTED_SENSORS = {
PROTOCOL_NAMES = {
CONF_MOISTURE: "R",
CONF_ACC: "Acc",
CONF_R_INT: "Rint",
CONF_R_INT: "RInt",
CONF_EVENT_ACC: "EventAcc",
CONF_TOTAL_ACC: "TotalAcc",
}

View File

View File

@ -0,0 +1,156 @@
#include "i2s_audio_media_player.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const char *const TAG = "audio";
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
if (call.get_media_url().has_value()) {
if (this->audio_->isRunning())
this->audio_->stopSong();
this->high_freq_.start();
this->audio_->connecttohost(call.get_media_url().value().c_str());
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
}
if (call.get_volume().has_value()) {
this->volume = call.get_volume().value();
this->set_volume_(volume);
this->unmute_();
}
if (call.get_command().has_value()) {
switch (call.get_command().value()) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
if (!this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
break;
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
if (this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
break;
case media_player::MEDIA_PLAYER_COMMAND_STOP:
this->stop_();
break;
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
this->mute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
this->unmute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
this->audio_->pauseResume();
if (this->audio_->isRunning()) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
}
break;
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
float new_volume = this->volume + 0.1f;
if (new_volume > 1.0f)
new_volume = 1.0f;
this->set_volume_(new_volume);
this->unmute_();
break;
}
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: {
float new_volume = this->volume - 0.1f;
if (new_volume < 0.0f)
new_volume = 0.0f;
this->set_volume_(new_volume);
this->unmute_();
break;
}
}
}
this->publish_state();
}
void I2SAudioMediaPlayer::mute_() {
if (this->mute_pin_ != nullptr) {
this->mute_pin_->digital_write(true);
} else {
this->set_volume_(0.0f, false);
}
this->muted_ = true;
}
void I2SAudioMediaPlayer::unmute_() {
if (this->mute_pin_ != nullptr) {
this->mute_pin_->digital_write(false);
} else {
this->set_volume_(this->volume, false);
}
this->muted_ = false;
}
void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) {
this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21));
if (publish)
this->volume = volume;
}
void I2SAudioMediaPlayer::stop_() {
if (this->audio_->isRunning())
this->audio_->stopSong();
this->high_freq_.stop();
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
}
void I2SAudioMediaPlayer::setup() {
ESP_LOGCONFIG(TAG, "Setting up Audio...");
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_);
} else {
this->audio_ = make_unique<Audio>(false);
this->audio_->setPinout(this->bclk_pin_, this->lrclk_pin_, this->dout_pin_);
this->audio_->forceMono(this->external_dac_channels_ == 1);
}
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
}
void I2SAudioMediaPlayer::loop() {
this->audio_->loop();
if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) {
this->stop_();
this->publish_state();
}
}
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
auto traits = media_player::MediaPlayerTraits();
traits.set_supports_pause(true);
return traits;
};
void I2SAudioMediaPlayer::dump_config() {
ESP_LOGCONFIG(TAG, "Audio:");
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, "Audio failed to initialize!");
return;
}
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
switch (this->internal_dac_mode_) {
case I2S_DAC_CHANNEL_LEFT_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left");
break;
case I2S_DAC_CHANNEL_RIGHT_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Right");
break;
case I2S_DAC_CHANNEL_BOTH_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left & Right");
break;
default:
break;
}
}
}
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@ -0,0 +1,63 @@
#pragma once
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/components/media_player/media_player.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include <Audio.h>
namespace esphome {
namespace i2s_audio {
class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer {
public:
void setup() override;
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
void loop() override;
void dump_config() override;
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
void set_bclk_pin(uint8_t pin) { this->bclk_pin_ = pin; }
void set_lrclk_pin(uint8_t pin) { this->lrclk_pin_ = pin; }
void set_mute_pin(GPIOPin *mute_pin) { this->mute_pin_ = mute_pin; }
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
media_player::MediaPlayerTraits get_traits() override;
bool is_muted() const override { return this->muted_; }
protected:
void control(const media_player::MediaPlayerCall &call) override;
void mute_();
void unmute_();
void set_volume_(float volume, bool publish = true);
void stop_();
std::unique_ptr<Audio> audio_;
uint8_t dout_pin_{0};
uint8_t din_pin_{0};
uint8_t bclk_pin_;
uint8_t lrclk_pin_;
GPIOPin *mute_pin_{nullptr};
bool muted_{false};
float unmuted_volume_{0};
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
uint8_t external_dac_channels_;
HighFrequencyLoopRequester high_freq_;
};
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@ -0,0 +1,94 @@
import esphome.codegen as cg
from esphome.components import media_player
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_MODE
from esphome.core import CORE
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioMediaPlayer = i2s_audio_ns.class_(
"I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer
)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
CONF_MUTE_PIN = "mute_pin"
CONF_AUDIO_ID = "audio_id"
CONF_DAC_TYPE = "dac_type"
INTERNAL_DAC_OPTIONS = {
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
)
.extend(media_player.MEDIA_PLAYER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
"external": cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Required(
CONF_I2S_BCLK_PIN
): pins.internal_gpio_output_pin_number,
cv.Required(
CONF_I2S_LRCLK_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
*EXTERNAL_DAC_OPTIONS, lower=True
),
}
)
.extend(media_player.MEDIA_PLAYER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
},
key=CONF_DAC_TYPE,
),
cv.only_with_arduino,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_player.register_media_player(var, config)
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_MUTE_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN])
cg.add(var.set_mute_pin(pin))
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
if CORE.is_esp32:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.1.0")
cg.add_build_flag("-DAUDIO_NO_SD_FS")

View File

@ -3,13 +3,16 @@ import esphome.config_validation as cv
from esphome import pins
from esphome.components import display, spi
from esphome.const import (
CONF_COLOR_PALETTE,
CONF_DC_PIN,
CONF_ID,
CONF_LAMBDA,
CONF_MODEL,
CONF_PAGES,
CONF_RAW_DATA_ID,
CONF_RESET_PIN,
)
from esphome.core import HexInt
DEPENDENCIES = ["spi"]
@ -21,16 +24,21 @@ ili9341 = ili9341_ns.class_(
)
ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341)
ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341)
ILI9341TFT24R = ili9341_ns.class_("ILI9341TFT24R", ili9341)
ILI9341Model = ili9341_ns.enum("ILI9341Model")
ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
MODELS = {
"M5STACK": ILI9341Model.M5STACK,
"TFT_2.4": ILI9341Model.TFT_24,
"TFT_2.4R": ILI9341Model.TFT_24R,
}
ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE")
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
{
@ -39,6 +47,8 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
.extend(cv.polling_component_schema("1s"))
@ -52,6 +62,8 @@ async def to_code(config):
lcd_type = ILI9341M5Stack
if config[CONF_MODEL] == "TFT_2.4":
lcd_type = ILI9341TFT24
if config[CONF_MODEL] == "TFT_2.4R":
lcd_type = ILI9341TFT24R
rhs = lcd_type.new()
var = cg.Pvariable(config[CONF_ID], rhs)
@ -73,3 +85,13 @@ async def to_code(config):
if CONF_LED_PIN in config:
led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
cg.add(var.set_led_pin(led_pin))
if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
rhs = []
for x in range(256):
rhs.extend([HexInt(x), HexInt(x), HexInt(x)])
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_palette(prog_arr))
else:
pass

View File

@ -112,29 +112,9 @@ void ILI9341Display::display_() {
this->y_high_ = 0;
}
uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) {
int r = color_8bit >> 5;
int g = (color_8bit >> 2) & 0x07;
int b = color_8bit & 0x03;
uint16_t color = (r * 0x04) << 11;
color |= (g * 0x09) << 5;
color |= (b * 0x0A);
return color;
}
uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) {
// convert 16bit color to 8 bit buffer
uint8_t r = color_16bit >> 11;
uint8_t g = (color_16bit >> 5) & 0x3F;
uint8_t b = color_16bit & 0x1F;
return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5));
}
void ILI9341Display::fill(Color color) {
auto color565 = display::ColorUtil::color_to_565(color);
memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_());
uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, color332, this->get_buffer_length_());
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
@ -181,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color)
this->y_high_ = (y > this->y_high_) ? y : this->y_high_;
uint32_t pos = (y * width_) + x;
auto color565 = display::ColorUtil::color_to_565(color);
buffer_[pos] = convert_to_8bit_color_(color565);
if (this->buffer_color_mode_ == BITS_8) {
uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
buffer_[pos] = color332;
} else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) {
uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_);
buffer_[pos] = index;
}
}
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
@ -247,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) {
}
for (uint32_t i = 0; i < sz; ++i) {
uint16_t color = convert_to_16bit_color_(*src++);
uint16_t color;
if (this->buffer_color_mode_ == BITS_8) {
color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++));
} else { // if (this->buffer_color_mode == BITS_8_INDEXED) {
Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_);
color = display::ColorUtil::color_to_565(col);
}
*dst++ = (uint8_t)(color >> 8);
*dst++ = (uint8_t) color;
}
@ -272,5 +263,13 @@ void ILI9341TFT24::initialize() {
this->fill_internal_(Color::BLACK);
}
// 24_TFT rotated display
void ILI9341TFT24R::initialize() {
this->init_lcd_(INITCMD_TFT);
this->width_ = 320;
this->height_ = 240;
this->fill_internal_(Color::BLACK);
}
} // namespace ili9341
} // namespace esphome

View File

@ -12,6 +12,12 @@ namespace ili9341 {
enum ILI9341Model {
M5STACK = 0,
TFT_24,
TFT_24R,
};
enum ILI9341ColorMode {
BITS_8,
BITS_8_INDEXED,
};
class ILI9341Display : public PollingComponent,
@ -24,6 +30,8 @@ class ILI9341Display : public PollingComponent,
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_led_pin(GPIOPin *led) { this->led_pin_ = led; }
void set_model(ILI9341Model model) { this->model_ = model; }
void set_palette(const uint8_t *palette) { this->palette_ = palette; }
void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; }
void command(uint8_t value);
void data(uint8_t value);
@ -41,6 +49,8 @@ class ILI9341Display : public PollingComponent,
this->initialize();
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void setup_pins_();
@ -51,8 +61,6 @@ class ILI9341Display : public PollingComponent,
void reset_();
void fill_internal_(Color color);
void display_();
uint16_t convert_to_16bit_color_(uint8_t color_8bit);
uint8_t convert_to_8bit_color_(uint16_t color_16bit);
ILI9341Model model_;
int16_t width_{320}; ///< Display width as modified by current rotation
@ -61,6 +69,9 @@ class ILI9341Display : public PollingComponent,
uint16_t y_low_{0};
uint16_t x_high_{0};
uint16_t y_high_{0};
const uint8_t *palette_;
ILI9341ColorMode buffer_color_mode_{BITS_8};
uint32_t get_buffer_length_();
int get_width_internal() override;
@ -92,5 +103,12 @@ class ILI9341TFT24 : public ILI9341Display {
public:
void initialize() override;
};
//----------- ILI9341_24_TFT rotated display --------------
class ILI9341TFT24R : public ILI9341Display {
public:
void initialize() override;
};
} // namespace ili9341
} // namespace esphome

View File

@ -86,6 +86,10 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public
void block_partial() { this->block_partial_ = true; }
display::DisplayType get_display_type() override {
return get_greyscale() ? display::DisplayType::DISPLAY_TYPE_GRAYSCALE : display::DisplayType::DISPLAY_TYPE_BINARY;
}
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void display1b_();

View File

@ -145,7 +145,6 @@ void LightState::loop() {
}
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
uint32_t LightState::hash_base() { return 1114400283; }
void LightState::publish_state() { this->remote_values_callback_.call(); }

View File

@ -150,8 +150,6 @@ class LightState : public EntityBase, public Component {
friend LightCall;
friend class AddressableLight;
uint32_t hash_base() override;
/// Internal method to start an effect with the given index
void start_effect_(uint32_t effect_index);
/// Internal method to get the currently active effect

View File

@ -57,7 +57,6 @@ void Lock::publish_state(LockState state) {
}
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
uint32_t Lock::hash_base() { return 856245656UL; }
void LockCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());

View File

@ -167,8 +167,6 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
uint32_t hash_base() override;
CallbackManager<void()> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;

View File

@ -178,7 +178,8 @@ void Logger::pre_setup() {
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
#endif
break;
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && \
!defined(USE_ESP32_VARIANT_ESP32S3)
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);

View File

@ -94,6 +94,14 @@ void MAX31865Sensor::read_data_() {
const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG);
this->write_config_(0b11000000, 0b00000000);
// Check for bad connection
if (rtd_resistance_register == 0b0000000000000000 || rtd_resistance_register == 0b1111111111111111) {
ESP_LOGE(TAG, "SPI bus read all 0 or all 1 (0x%04X), check MAX31865 wiring & power.", rtd_resistance_register);
this->publish_state(NAN);
this->status_set_error();
return;
}
// Check faults
const uint8_t faults = this->read_register_(FAULT_STATUS_REG);
if ((has_fault_ = faults & 0b00111100)) {

View File

@ -11,6 +11,9 @@ from esphome.const import (
UNIT_CELSIUS,
)
CODEOWNERS = ["@DAVe3283"]
DEPENDENCIES = ["spi"]
max31865_ns = cg.esphome_ns.namespace("max31865")
MAX31865Sensor = max31865_ns.class_(
"MAX31865Sensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice

View File

@ -93,6 +93,8 @@ class MAX7219Component : public PollingComponent,
uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0)));
#endif
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void send_byte_(uint8_t a_register, uint8_t data);
void send_to_all_(uint8_t a_register, uint8_t data);

View File

@ -0,0 +1,104 @@
from esphome import automation
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID
from esphome.core import CORE
from esphome.coroutine import coroutine_with_priority
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@jesserockz"]
IS_PLATFORM_COMPONENT = True
media_player_ns = cg.esphome_ns.namespace("media_player")
MediaPlayer = media_player_ns.class_("MediaPlayer")
PlayAction = media_player_ns.class_(
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
)
ToggleAction = media_player_ns.class_(
"ToggleAction", automation.Action, cg.Parented.template(MediaPlayer)
)
PauseAction = media_player_ns.class_(
"PauseAction", automation.Action, cg.Parented.template(MediaPlayer)
)
StopAction = media_player_ns.class_(
"StopAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeUpAction = media_player_ns.class_(
"VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeDownAction = media_player_ns.class_(
"VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
CONF_VOLUME = "volume"
async def setup_media_player_core_(var, config):
await setup_entity(var, config)
async def register_media_player(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_media_player(var))
await setup_media_player_core_(var, config)
MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.Schema({}))
MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)})
@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA)
@automation.register_action(
"media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA)
@automation.register_action(
"media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA
)
async def media_player_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"media_player.volume_set",
VolumeSetAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Required(CONF_VOLUME): cv.templatable(cv.percentage),
},
key=CONF_VOLUME,
),
)
async def media_player_volume_set_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
volume = await cg.templatable(config[CONF_VOLUME], args, float)
cg.add(var.set_volume(volume))
return var
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_global(media_player_ns.using)
cg.add_define("USE_MEDIA_PLAYER")

View File

@ -0,0 +1,30 @@
#pragma once
#include "esphome/core/automation.h"
#include "media_player.h"
namespace esphome {
namespace media_player {
#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \
template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<MediaPlayer> { \
void play(Ts... x) override { \
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \
} \
};
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP)
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN)
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(float, volume)
void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); }
};
} // namespace media_player
} // namespace esphome

View File

@ -0,0 +1,118 @@
#include "media_player.h"
#include "esphome/core/log.h"
namespace esphome {
namespace media_player {
static const char *const TAG = "media_player";
const char *media_player_state_to_string(MediaPlayerState state) {
switch (state) {
case MEDIA_PLAYER_STATE_IDLE:
return "IDLE";
case MEDIA_PLAYER_STATE_PLAYING:
return "PLAYING";
case MEDIA_PLAYER_STATE_PAUSED:
return "PAUSED";
case MEDIA_PLAYER_STATE_NONE:
default:
return "UNKNOWN";
}
}
const char *media_player_command_to_string(MediaPlayerCommand command) {
switch (command) {
case MEDIA_PLAYER_COMMAND_PLAY:
return "PLAY";
case MEDIA_PLAYER_COMMAND_PAUSE:
return "PAUSE";
case MEDIA_PLAYER_COMMAND_STOP:
return "STOP";
case MEDIA_PLAYER_COMMAND_MUTE:
return "MUTE";
case MEDIA_PLAYER_COMMAND_UNMUTE:
return "UNMUTE";
case MEDIA_PLAYER_COMMAND_TOGGLE:
return "TOGGLE";
default:
return "UNKNOWN";
}
}
void MediaPlayerCall::validate_() {
if (this->media_url_.has_value()) {
if (this->command_.has_value()) {
ESP_LOGW(TAG, "MediaPlayerCall: Setting both command and media_url is not needed.");
this->command_.reset();
}
}
if (this->volume_.has_value()) {
if (this->volume_.value() < 0.0f || this->volume_.value() > 1.0f) {
ESP_LOGW(TAG, "MediaPlayerCall: Volume must be between 0.0 and 1.0.");
this->volume_.reset();
}
}
}
void MediaPlayerCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
this->validate_();
if (this->command_.has_value()) {
const char *command_s = media_player_command_to_string(this->command_.value());
ESP_LOGD(TAG, " Command: %s", command_s);
}
if (this->media_url_.has_value()) {
ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str());
}
if (this->volume_.has_value()) {
ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value());
}
this->parent_->control(*this);
}
MediaPlayerCall &MediaPlayerCall::set_command(MediaPlayerCommand command) {
this->command_ = command;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_command(optional<MediaPlayerCommand> command) {
this->command_ = command;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) {
if (str_equals_case_insensitive(command, "PLAY")) {
this->set_command(MEDIA_PLAYER_COMMAND_PLAY);
} else if (str_equals_case_insensitive(command, "PAUSE")) {
this->set_command(MEDIA_PLAYER_COMMAND_PAUSE);
} else if (str_equals_case_insensitive(command, "STOP")) {
this->set_command(MEDIA_PLAYER_COMMAND_STOP);
} else if (str_equals_case_insensitive(command, "MUTE")) {
this->set_command(MEDIA_PLAYER_COMMAND_MUTE);
} else if (str_equals_case_insensitive(command, "UNMUTE")) {
this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE);
} else if (str_equals_case_insensitive(command, "TOGGLE")) {
this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str());
}
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_media_url(const std::string &media_url) {
this->media_url_ = media_url;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_volume(float volume) {
this->volume_ = volume;
return *this;
}
void MediaPlayer::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void MediaPlayer::publish_state() { this->state_callback_.call(); }
} // namespace media_player
} // namespace esphome

View File

@ -0,0 +1,93 @@
#pragma once
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace media_player {
enum MediaPlayerState : uint8_t {
MEDIA_PLAYER_STATE_NONE = 0,
MEDIA_PLAYER_STATE_IDLE = 1,
MEDIA_PLAYER_STATE_PLAYING = 2,
MEDIA_PLAYER_STATE_PAUSED = 3
};
const char *media_player_state_to_string(MediaPlayerState state);
enum MediaPlayerCommand : uint8_t {
MEDIA_PLAYER_COMMAND_PLAY = 0,
MEDIA_PLAYER_COMMAND_PAUSE = 1,
MEDIA_PLAYER_COMMAND_STOP = 2,
MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
};
const char *media_player_command_to_string(MediaPlayerCommand command);
class MediaPlayer;
class MediaPlayerTraits {
public:
MediaPlayerTraits() = default;
void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; }
bool get_supports_pause() const { return this->supports_pause_; }
protected:
bool supports_pause_{false};
};
class MediaPlayerCall {
public:
MediaPlayerCall(MediaPlayer *parent) : parent_(parent) {}
MediaPlayerCall &set_command(MediaPlayerCommand command);
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
MediaPlayerCall &set_command(const std::string &command);
MediaPlayerCall &set_media_url(const std::string &url);
MediaPlayerCall &set_volume(float volume);
void perform();
const optional<MediaPlayerCommand> &get_command() const { return command_; }
const optional<std::string> &get_media_url() const { return media_url_; }
const optional<float> &get_volume() const { return volume_; }
protected:
void validate_();
MediaPlayer *const parent_;
optional<MediaPlayerCommand> command_;
optional<std::string> media_url_;
optional<float> volume_;
};
class MediaPlayer : public EntityBase {
public:
MediaPlayerState state{MEDIA_PLAYER_STATE_NONE};
float volume{1.0f};
MediaPlayerCall make_call() { return MediaPlayerCall(this); }
void publish_state();
void add_on_state_callback(std::function<void()> &&callback);
virtual bool is_muted() const { return false; }
virtual MediaPlayerTraits get_traits() = 0;
protected:
friend MediaPlayerCall;
virtual void control(const MediaPlayerCall &call) = 0;
CallbackManager<void()> state_callback_{};
};
} // namespace media_player
} // namespace esphome

View File

@ -56,6 +56,11 @@ template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
void play(Ts... x) override { this->parent_->do_power_off(); }
};
template<typename... Ts> class PowerToggleAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_power_toggle(); }
};
} // namespace ac
} // namespace midea
} // namespace esphome

View File

@ -39,6 +39,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void do_beeper_off() { this->set_beeper_feedback(false); }
void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); }
void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; }

View File

@ -113,7 +113,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
@ -163,6 +163,7 @@ BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action)
BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action)
PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action)
PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action)
PowerToggleAction = midea_ac_ns.class_("PowerToggleAction", automation.Action)
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
{
@ -249,6 +250,16 @@ async def power_off_to_code(var, config, args):
pass
# Power Toggle action
@register_action(
"power_toggle",
PowerToggleAction,
cv.Schema({}),
)
async def power_inv_to_code(var, config, args):
pass
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@ -68,33 +68,54 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
uint8_t data_len = raw[2];
uint8_t data_offset = 3;
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
data_offset = 2;
data_len = 4;
}
// Error ( msb indicates error )
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
if ((function_code & 0x80) == 0x80) {
data_offset = 2;
data_len = 1;
}
// Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) {
// Handle user-defined function, since we don't know how big this ought to be,
// ideally we should delegate the entire length detection to whatever handler is
// installed, but wait, there is the CRC, and if we get a hit there is a good
// chance that this is a complete message ... admittedly there is a small chance is
// isn't but that is quite small given the purpose of the CRC in the first place
data_len = at;
data_offset = 1;
// Byte data_offset..data_offset+data_len-1: Data
if (at < data_offset + data_len)
return true;
uint16_t computed_crc = crc16(raw, data_offset + data_len);
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
// Byte 3+data_len: CRC_LO (over all bytes)
if (at == data_offset + data_len)
return true;
if (computed_crc != remote_crc)
return true;
// Byte data_offset+len+1: CRC_HI (over all bytes)
uint16_t computed_crc = crc16(raw, data_offset + data_len);
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
if (computed_crc != remote_crc) {
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
return false;
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
} else {
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
data_offset = 2;
data_len = 4;
}
// Error ( msb indicates error )
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
if ((function_code & 0x80) == 0x80) {
data_offset = 2;
data_len = 1;
}
// Byte data_offset..data_offset+data_len-1: Data
if (at < data_offset + data_len)
return true;
// Byte 3+data_len: CRC_LO (over all bytes)
if (at == data_offset + data_len)
return true;
// Byte data_offset+len+1: CRC_HI (over all bytes)
uint16_t computed_crc = crc16(raw, data_offset + data_len);
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
if (computed_crc != remote_crc) {
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
return false;
}
}
std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
bool found = false;

View File

@ -24,6 +24,8 @@ from esphome.const import (
CONF_LOG_TOPIC,
CONF_ON_JSON_MESSAGE,
CONF_ON_MESSAGE,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_PAYLOAD_AVAILABLE,
@ -90,6 +92,10 @@ MQTTMessageTrigger = mqtt_ns.class_(
MQTTJsonMessageTrigger = mqtt_ns.class_(
"MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst)
)
MQTTConnectTrigger = mqtt_ns.class_("MQTTConnectTrigger", automation.Trigger.template())
MQTTDisconnectTrigger = mqtt_ns.class_(
"MQTTDisconnectTrigger", automation.Trigger.template()
)
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
@ -212,6 +218,18 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTConnectTrigger),
}
),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
MQTTDisconnectTrigger
),
}
),
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger),
@ -362,6 +380,14 @@ async def to_code(config):
trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS])
await automation.build_automation(trig, [(cg.JsonObjectConst, "x")], conf)
for conf in config.get(CONF_ON_CONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISCONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
{

View File

@ -572,6 +572,14 @@ void MQTTClientComponent::on_shutdown() {
this->mqtt_backend_.disconnect();
}
void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) {
this->mqtt_backend_.set_on_connect(std::forward<mqtt_on_connect_callback_t>(callback));
}
void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) {
this->mqtt_backend_.set_on_disconnect(std::forward<mqtt_on_disconnect_callback_t>(callback));
}
#if ASYNC_TCP_SSL_ENABLED
void MQTTClientComponent::add_ssl_fingerprint(const std::array<uint8_t, SHA1_SIZE> &fingerprint) {
this->mqtt_backend_.setSecure(true);

View File

@ -19,6 +19,11 @@
namespace esphome {
namespace mqtt {
/** Callback for MQTT events.
*/
using mqtt_on_connect_callback_t = std::function<MQTTBackend::on_connect_callback_t>;
using mqtt_on_disconnect_callback_t = std::function<MQTTBackend::on_disconnect_callback_t>;
/** Callback for MQTT subscriptions.
*
* First parameter is the topic, the second one is the payload.
@ -240,6 +245,8 @@ class MQTTClientComponent : public Component {
void set_username(const std::string &username) { this->credentials_.username = username; }
void set_password(const std::string &password) { this->credentials_.password = password; }
void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; }
void set_on_connect(mqtt_on_connect_callback_t &&callback);
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
protected:
/// Reconnect to the MQTT broker if not already connected.
@ -328,6 +335,20 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> {
}
};
class MQTTConnectTrigger : public Trigger<> {
public:
explicit MQTTConnectTrigger(MQTTClientComponent *&client) {
client->set_on_connect([this](bool session_present) { this->trigger(); });
}
};
class MQTTDisconnectTrigger : public Trigger<> {
public:
explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) {
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(); });
}
};
template<typename... Ts> class MQTTPublishAction : public Action<Ts...> {
public:
MQTTPublishAction(MQTTClientComponent *parent) : parent_(parent) {}

View File

@ -51,10 +51,9 @@ void MQTTCoverComponent::setup() {
void MQTTCoverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
auto traits = this->cover_->get_traits();
// no state topic for position
bool state_topic = !traits.get_supports_position();
LOG_MQTT_COMPONENT(state_topic, true)
if (!state_topic) {
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic)
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str());
}
@ -72,7 +71,6 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
root[MQTT_OPTIMISTIC] = true;
}
if (traits.get_supports_position()) {
config.state_topic = false;
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
}
@ -92,17 +90,7 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
bool success = true;
if (!traits.get_supports_position()) {
const char *state_s = "unknown";
if (this->cover_->position == COVER_OPEN) {
state_s = "open";
} else if (this->cover_->position == COVER_CLOSED) {
state_s = "closed";
}
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
} else {
if (traits.get_supports_position()) {
std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos))
success = false;
@ -112,6 +100,14 @@ bool MQTTCoverComponent::publish_state() {
if (!this->publish(this->get_tilt_state_topic(), pos))
success = false;
}
const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening"
: this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing"
: this->cover_->position == COVER_CLOSED ? "closed"
: this->cover_->position == COVER_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
success = false;
return success;
}

View File

@ -5,7 +5,6 @@
#ifdef USE_MQTT
#ifdef USE_FAN
#include "esphome/components/fan/fan_helpers.h"
namespace esphome {
namespace mqtt {
@ -88,17 +87,6 @@ void MQTTFanComponent::setup() {
});
}
if (this->state_->get_traits().supports_speed()) {
this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->state_->make_call()
.set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations)
.perform();
#pragma GCC diagnostic pop
});
}
auto f = std::bind(&MQTTFanComponent::publish_state, this);
this->state_->add_on_state_callback([this, f]() { this->defer("send", f); });
}
@ -113,8 +101,6 @@ void MQTTFanComponent::dump_config() {
if (this->state_->get_traits().supports_speed()) {
ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str());
ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str());
ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str());
}
}
@ -126,10 +112,9 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic();
}
if (this->state_->get_traits().supports_speed()) {
root["speed_level_command_topic"] = this->get_speed_level_command_topic();
root["speed_level_state_topic"] = this->get_speed_level_state_topic();
root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic();
root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic();
root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic();
root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic();
root[MQTT_SPEED_RANGE_MAX] = this->state_->get_traits().supported_speed_count();
}
}
bool MQTTFanComponent::publish_state() {
@ -148,31 +133,6 @@ bool MQTTFanComponent::publish_state() {
bool success = this->publish(this->get_speed_level_state_topic(), payload);
failed = failed || !success;
}
if (traits.supports_speed()) {
const char *payload;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) {
case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "low";
break;
}
case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "medium";
break;
}
default:
case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "high";
break;
}
}
#pragma GCC diagnostic pop
bool success = this->publish(this->get_speed_state_topic(), payload);
failed = failed || !success;
}
return !failed;
}

View File

@ -115,7 +115,7 @@ void Nextion::set_backlight_brightness(float brightness) {
ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0");
return;
}
this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast<int>(brightness * 100));
this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100));
}
void Nextion::set_auto_wake_on_touch(bool auto_wake) {

View File

@ -17,7 +17,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
this->state_callback_.add(std::move(callback));
}
uint32_t Number::hash_base() { return 2282307003UL; }
} // namespace number
} // namespace esphome

View File

@ -52,8 +52,6 @@ class Number : public EntityBase {
*/
virtual void control(float value) = 0;
uint32_t hash_base() override;
CallbackManager<void(float)> state_callback_;
bool has_state_{false};
};

View File

@ -52,6 +52,8 @@ class PCD8544 : public PollingComponent,
this->initialize();
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;

View File

@ -1,6 +1,9 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ID
from esphome.const import (
CONF_ID,
CONF_INCLUDE_INTERNAL,
)
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.components import web_server_base
@ -15,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase
),
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
},
cv.only_with_arduino,
).extend(cv.COMPONENT_SCHEMA)
@ -27,3 +31,5 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID], paren)
await cg.register_component(var, config)
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))

View File

@ -61,7 +61,7 @@ void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_sensor_failed GAUGE\n"));
}
void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
if (!std::isnan(obj->state)) {
// We have a valid value, output this value
@ -98,7 +98,7 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n"));
}
void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->has_state()) {
// We have a valid value, output this value
@ -134,7 +134,7 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n"));
}
void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_fan_failed{id=\""));
stream->print(obj->get_object_id().c_str());
@ -179,7 +179,7 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_light_effect_active GAUGE\n"));
}
void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
// State
stream->print(F("esphome_light_state{id=\""));
@ -255,7 +255,7 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_cover_failed GAUGE\n"));
}
void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
if (!std::isnan(obj->position)) {
// We have a valid value, output this value
@ -298,7 +298,7 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_switch_failed GAUGE\n"));
}
void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_switch_failed{id=\""));
stream->print(obj->get_object_id().c_str());
@ -322,7 +322,7 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_lock_failed GAUGE\n"));
}
void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) {
if (obj->is_internal())
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_lock_failed{id=\""));
stream->print(obj->get_object_id().c_str());

View File

@ -13,6 +13,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
public:
PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {}
/** Determine whether internal components should be exported as metrics.
* Defaults to false.
*
* @param include_internal Whether internal components should be exported.
*/
void set_include_internal(bool include_internal) { include_internal_ = include_internal; }
bool canHandle(AsyncWebServerRequest *request) override {
if (request->method() == HTTP_GET) {
if (request->url() == "/metrics")
@ -84,6 +91,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
#endif
web_server_base::WebServerBase *base_;
bool include_internal_{false};
};
} // namespace prometheus

View File

@ -728,6 +728,48 @@ async def rc5_action(var, config, args):
cg.add(var.set_command(template_))
# RC6
RC6Data, RC6BinarySensor, RC6Trigger, RC6Action, RC6Dumper = declare_protocol("RC6")
RC6_SCHEMA = cv.Schema(
{
cv.Required(CONF_ADDRESS): cv.hex_uint8_t,
cv.Required(CONF_COMMAND): cv.hex_uint8_t,
}
)
@register_binary_sensor("rc6", RC6BinarySensor, RC6_SCHEMA)
def rc6_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
RC6Data,
("device", config[CONF_DEVICE]),
("address", config[CONF_ADDRESS]),
("command", config[CONF_COMMAND]),
)
)
)
@register_trigger("rc6", RC6Trigger, RC6Data)
def rc6_trigger(var, config):
pass
@register_dumper("rc6", RC6Dumper)
def rc6_dumper(var, config):
pass
@register_action("rc6", RC6Action, RC6_SCHEMA)
async def rc6_action(var, config, args):
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
cg.add(var.set_address(template_))
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
cg.add(var.set_command(template_))
# RC Switch Raw
RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2))

View File

@ -0,0 +1,181 @@
#include "rc6_protocol.h"
#include "esphome/core/log.h"
namespace esphome {
namespace remote_base {
static const char *const RC6_TAG = "remote.rc6";
static const uint16_t RC6_FREQ = 36000;
static const uint16_t RC6_UNIT = 444;
static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT);
static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT);
static const uint16_t RC6_MODE_MASK = 0x07;
void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) {
dst->reserve(44);
dst->set_carrier_frequency(RC6_FREQ);
// Encode header
dst->item(RC6_HEADER_MARK, RC6_HEADER_SPACE);
int32_t next{0};
// Encode startbit+mode
uint8_t header{static_cast<uint8_t>((1 << 3) | data.mode)};
for (uint8_t mask = 0x8; mask; mask >>= 1) {
if (header & mask) {
if (next < 0) {
dst->space(-next);
next = 0;
}
if (next >= 0) {
next = next + RC6_UNIT;
dst->mark(next);
next = -RC6_UNIT;
}
} else {
if (next > 0) {
dst->mark(next);
next = 0;
}
if (next <= 0) {
next = next - RC6_UNIT;
dst->space(-next);
next = RC6_UNIT;
}
}
}
// Toggle
if (data.toggle) {
if (next < 0) {
dst->space(-next);
next = 0;
}
if (next >= 0) {
next = next + RC6_UNIT * 2;
dst->mark(next);
next = -RC6_UNIT * 2;
}
} else {
if (next > 0) {
dst->mark(next);
next = 0;
}
if (next <= 0) {
next = next - RC6_UNIT * 2;
dst->space(-next);
next = RC6_UNIT * 2;
}
}
// Encode data
uint16_t raw{static_cast<uint16_t>((data.address << 8) | data.command)};
for (uint16_t mask = 0x8000; mask; mask >>= 1) {
if (raw & mask) {
if (next < 0) {
dst->space(-next);
next = 0;
}
if (next >= 0) {
next = next + RC6_UNIT;
dst->mark(next);
next = -RC6_UNIT;
}
} else {
if (next > 0) {
dst->mark(next);
next = 0;
}
if (next <= 0) {
next = next - RC6_UNIT;
dst->space(-next);
next = RC6_UNIT;
}
}
}
if (next > 0) {
dst->mark(next);
} else {
dst->space(-next);
}
}
optional<RC6Data> RC6Protocol::decode(RemoteReceiveData src) {
RC6Data data{
.mode = 0,
.toggle = 0,
.address = 0,
.command = 0,
};
// Check if header matches
if (!src.expect_item(RC6_HEADER_MARK, RC6_HEADER_SPACE)) {
return {};
}
uint8_t bit{1};
uint8_t offset{0};
uint8_t header{0};
uint32_t buffer{0};
// Startbit + mode
while (offset < 4) {
bit = src.peek() > 0;
header = header + (bit << (3 - offset++));
src.advance();
if (src.peek_mark(RC6_UNIT) || src.peek_space(RC6_UNIT)) {
src.advance();
} else if (offset == 4) {
break;
} else if (!src.peek_mark(RC6_UNIT * 2) && !src.peek_space(RC6_UNIT * 2)) {
return {};
}
}
data.mode = header & RC6_MODE_MASK;
if (data.mode != 0) {
return {}; // I dont have a device to test other modes
}
// Toggle
data.toggle = src.peek() > 0;
src.advance();
if (src.peek_mark(RC6_UNIT * 2) || src.peek_space(RC6_UNIT * 2)) {
src.advance();
}
// Data
offset = 0;
while (offset < 16) {
bit = src.peek() > 0;
buffer = buffer + (bit << (15 - offset++));
src.advance();
if (offset == 16) {
break;
} else if (src.peek_mark(RC6_UNIT) || src.peek_space(RC6_UNIT)) {
src.advance();
} else if (!src.peek_mark(RC6_UNIT * 2) && !src.peek_space(RC6_UNIT * 2)) {
return {};
}
}
data.address = (0xFF00 & buffer) >> 8;
data.command = (0x00FF & buffer);
return data;
}
void RC6Protocol::dump(const RC6Data &data) {
ESP_LOGD(RC6_TAG, "Received RC6: mode=0x%X, address=0x%02X, command=0x%02X, toggle=0x%X", data.mode, data.address,
data.command, data.toggle);
}
} // namespace remote_base
} // namespace esphome

View File

@ -0,0 +1,46 @@
#pragma once
#include "remote_base.h"
namespace esphome {
namespace remote_base {
struct RC6Data {
uint8_t mode : 3;
uint8_t toggle : 1;
uint8_t address;
uint8_t command;
bool operator==(const RC6Data &rhs) const { return address == rhs.address && command == rhs.command; }
};
class RC6Protocol : public RemoteProtocol<RC6Data> {
public:
void encode(RemoteTransmitData *dst, const RC6Data &data) override;
optional<RC6Data> decode(RemoteReceiveData src) override;
void dump(const RC6Data &data) override;
};
DECLARE_REMOTE_PROTOCOL(RC6)
template<typename... Ts> class RC6Action : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint8_t, address)
TEMPLATABLE_VALUE(uint8_t, command)
void encode(RemoteTransmitData *dst, Ts... x) {
RC6Data data{};
data.mode = 0;
data.toggle = this->toggle_;
data.address = this->address_.value(x...);
data.command = this->command_.value(x...);
RC6Protocol().encode(dst, data);
this->toggle_ = !this->toggle_;
}
protected:
uint8_t toggle_{0};
};
} // namespace remote_base
} // namespace esphome

View File

@ -103,7 +103,7 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore
rotation_dir = -1;
}
if (rotation_dir != 0) {
if (rotation_dir != 0 && !arg->first_read) {
auto *first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero
if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration)
|| std::signbit(*std::prev(first_zero)) !=
@ -119,6 +119,7 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore
*std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot
}
}
arg->first_read = false;
arg->state = new_state;
}

View File

@ -34,6 +34,7 @@ struct RotaryEncoderSensorStore {
int32_t max_value{INT32_MAX};
int32_t last_read{0};
uint8_t state{0};
bool first_read{true};
std::array<int8_t, 8> rotation_events{};
bool rotation_events_overflow{false};

View File

@ -11,7 +11,7 @@ template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts
public:
void play(Ts... x) override {
if (this->value_.has_value()) {
this->parent_->perform_forced_calibration(value_.value());
this->parent_->perform_forced_calibration(this->value_.value(x...));
}
}

View File

@ -40,7 +40,7 @@ void SDP3XComponent::setup() {
}
uint16_t data[6];
if (this->read_data(data, 6) != i2c::ERROR_OK) {
if (!this->read_data(data, 6)) {
ESP_LOGE(TAG, "Read ID SDP3X failed!");
this->mark_failed();
return;
@ -78,8 +78,7 @@ void SDP3XComponent::setup() {
}
}
if (this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG) !=
i2c::ERROR_OK) {
if (!this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG)) {
ESP_LOGE(TAG, "Start Measurements SDP3X failed!");
this->mark_failed();
return;
@ -98,7 +97,7 @@ void SDP3XComponent::dump_config() {
void SDP3XComponent::read_pressure_() {
uint16_t data[3];
if (this->read_data(data, 3) != i2c::ERROR_OK) {
if (!this->read_data(data, 3)) {
ESP_LOGW(TAG, "Couldn't read SDP3X data!");
this->status_set_warning();
return;

View File

@ -58,7 +58,5 @@ optional<std::string> Select::at(size_t index) const {
}
}
uint32_t Select::hash_base() { return 2812997003UL; }
} // namespace select
} // namespace esphome

View File

@ -65,8 +65,6 @@ class Select : public EntityBase {
*/
virtual void control(const std::string &value) = 0;
uint32_t hash_base() override;
CallbackManager<void(std::string, size_t)> state_callback_;
bool has_state_{false};
};

View File

@ -126,7 +126,6 @@ void Sensor::internal_send_state_to_frontend(float state) {
this->callback_.call(state);
}
bool Sensor::has_state() const { return this->has_state_; }
uint32_t Sensor::hash_base() { return 2455723294UL; }
} // namespace sensor
} // namespace esphome

View File

@ -174,8 +174,6 @@ class Sensor : public EntityBase {
*/
virtual StateClass state_class(); // NOLINT
uint32_t hash_base() override;
CallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.

View File

@ -1,628 +0,0 @@
#include "sensirion_voc_algorithm.h"
namespace esphome {
namespace sgp40 {
/* The VOC code were originally created by
* https://github.com/Sensirion/embedded-sgp
* The fixed point arithmetic parts of this code were originally created by
* https://github.com/PetteriAimonen/libfixmath
*/
/*!< the maximum value of fix16_t */
#define FIX16_MAXIMUM 0x7FFFFFFF
/*!< the minimum value of fix16_t */
static const uint32_t FIX16_MINIMUM = 0x80000000;
/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not
* specified */
static const uint32_t FIX16_OVERFLOW = 0x80000000;
/*!< fix16_t value of 1 */
const uint32_t FIX16_ONE = 0x00010000;
inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; }
inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); }
/*! Multiplies the two given fix16_t's and returns the result. */
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1);
/*! Divides the first given fix16_t by the second and returns the result. */
static fix16_t fix16_div(fix16_t a, fix16_t b);
/*! Returns the square root of the given fix16_t. */
static fix16_t fix16_sqrt(fix16_t in_value);
/*! Returns the exponent (e^) of the given fix16_t. */
static fix16_t fix16_exp(fix16_t in_value);
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) {
// Each argument is divided to 16-bit parts.
// AB
// * CD
// -----------
// BD 16 * 16 -> 32 bit products
// CB
// AD
// AC
// |----| 64 bit product
int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16);
uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF);
int32_t ac = a * c;
int32_t ad_cb = a * d + c * b;
uint32_t bd = b * d;
int32_t product_hi = ac + (ad_cb >> 16); // NOLINT
// Handle carry from lower 32 bits to upper part of result.
uint32_t ad_cb_temp = ad_cb << 16; // NOLINT
uint32_t product_lo = bd + ad_cb_temp;
if (product_lo < bd)
product_hi++;
#ifndef FIXMATH_NO_OVERFLOW
// The upper 17 bits should all be the same (the sign).
if (product_hi >> 31 != product_hi >> 15)
return FIX16_OVERFLOW;
#endif
#ifdef FIXMATH_NO_ROUNDING
return (product_hi << 16) | (product_lo >> 16);
#else
// Subtracting 0x8000 (= 0.5) and then using signed right shift
// achieves proper rounding to result-1, except in the corner
// case of negative numbers and lowest word = 0x8000.
// To handle that, we also have to subtract 1 for negative numbers.
uint32_t product_lo_tmp = product_lo;
product_lo -= 0x8000;
product_lo -= (uint32_t) product_hi >> 31;
if (product_lo > product_lo_tmp)
product_hi--;
// Discard the lowest 16 bits. Note that this is not exactly the same
// as dividing by 0x10000. For example if product = -1, result will
// also be -1 and not 0. This is compensated by adding +1 to the result
// and compensating this in turn in the rounding above.
fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT
result += 1;
return result;
#endif
}
static fix16_t fix16_div(fix16_t a, fix16_t b) {
// This uses the basic binary restoring division algorithm.
// It appears to be faster to do the whole division manually than
// trying to compose a 64-bit divide out of 32-bit divisions on
// platforms without hardware divide.
if (b == 0)
return FIX16_MINIMUM;
uint32_t remainder = (a >= 0) ? a : (-a);
uint32_t divider = (b >= 0) ? b : (-b);
uint32_t quotient = 0;
uint32_t bit = 0x10000;
/* The algorithm requires D >= R */
while (divider < remainder) {
divider <<= 1;
bit <<= 1;
}
#ifndef FIXMATH_NO_OVERFLOW
if (!bit)
return FIX16_OVERFLOW;
#endif
if (divider & 0x80000000) {
// Perform one step manually to avoid overflows later.
// We know that divider's bottom bit is 0 here.
if (remainder >= divider) {
quotient |= bit;
remainder -= divider;
}
divider >>= 1;
bit >>= 1;
}
/* Main division loop */
while (bit && remainder) {
if (remainder >= divider) {
quotient |= bit;
remainder -= divider;
}
remainder <<= 1;
bit >>= 1;
}
#ifndef FIXMATH_NO_ROUNDING
if (remainder >= divider) {
quotient++;
}
#endif
fix16_t result = quotient;
/* Figure out the sign of result */
if ((a ^ b) & 0x80000000) {
#ifndef FIXMATH_NO_OVERFLOW
if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare)
return FIX16_OVERFLOW;
#endif
result = -result;
}
return result;
}
static fix16_t fix16_sqrt(fix16_t in_value) {
// It is assumed that x is not negative
uint32_t num = in_value;
uint32_t result = 0;
uint32_t bit;
uint8_t n;
bit = (uint32_t) 1 << 30;
while (bit > num)
bit >>= 2;
// The main part is executed twice, in order to avoid
// using 64 bit values in computations.
for (n = 0; n < 2; n++) {
// First we get the top 24 bits of the answer.
while (bit) {
if (num >= result + bit) {
num -= result + bit;
result = (result >> 1) + bit;
} else {
result = (result >> 1);
}
bit >>= 2;
}
if (n == 0) {
// Then process it again to get the lowest 8 bits.
if (num > 65535) {
// The remainder 'num' is too large to be shifted left
// by 16, so we have to add 1 to result manually and
// adjust 'num' accordingly.
// num = a - (result + 0.5)^2
// = num + result^2 - (result + 0.5)^2
// = num - result - 0.5
num -= result;
num = (num << 16) - 0x8000;
result = (result << 16) + 0x8000;
} else {
num <<= 16;
result <<= 16;
}
bit = 1 << 14;
}
}
#ifndef FIXMATH_NO_ROUNDING
// Finally, if next bit would have been 1, round the result upwards.
if (num > result) {
result++;
}
#endif
return (fix16_t) result;
}
static fix16_t fix16_exp(fix16_t in_value) {
// Function to approximate exp(); optimized more for code size than speed
// exp(x) for x = +/- {1, 1/8, 1/64, 1/512}
fix16_t x = in_value;
static const uint8_t NUM_EXP_VALUES = 4;
static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)};
static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)};
const fix16_t *exp_values;
fix16_t res, arg;
uint16_t i;
if (x >= F16(10.3972))
return FIX16_MAXIMUM;
if (x <= F16(-11.7835))
return 0;
if (x < 0) {
x = -x;
exp_values = EXP_NEG_VALUES;
} else {
exp_values = EXP_POS_VALUES;
}
res = FIX16_ONE;
arg = FIX16_ONE;
for (i = 0; i < NUM_EXP_VALUES; i++) {
while (x >= arg) {
res = fix16_mul(res, exp_values[i]);
x -= arg;
}
arg >>= 3;
}
return res;
}
static void voc_algorithm_init_instances(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
fix16_t tau_mean_variance_hours,
fix16_t gating_max_duration_minutes);
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
fix16_t uptime_gamma);
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params);
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
fix16_t voc_index_from_prior);
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
fix16_t voc_index_from_prior);
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
fix16_t x0, fix16_t k);
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample);
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params);
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean);
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw);
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params);
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset);
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample);
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params);
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params);
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample);
void voc_algorithm_init(VocAlgorithmParams *params) {
params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT);
params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS);
params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES);
params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL);
params->mUptime = F16(0.);
params->mSraw = F16(0.);
params->mVoc_Index = 0;
voc_algorithm_init_instances(params);
}
static void voc_algorithm_init_instances(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_init(params);
voc_algorithm_mean_variance_estimator_set_parameters(
params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes);
voc_algorithm_mox_model_init(params);
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
voc_algorithm_mean_variance_estimator_get_mean(params));
voc_algorithm_sigmoid_scaled_init(params);
voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset);
voc_algorithm_adaptive_lowpass_init(params);
voc_algorithm_adaptive_lowpass_set_parameters(params);
}
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) {
*state0 = voc_algorithm_mean_variance_estimator_get_mean(params);
*state1 = voc_algorithm_mean_variance_estimator_get_std(params);
}
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) {
voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA));
params->mSraw = state0;
}
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
int32_t std_initial) {
params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset));
params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours));
params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes));
params->mSraw_Std_Initial = (fix16_from_int(std_initial));
voc_algorithm_init_instances(params);
}
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) {
if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) {
params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
} else {
if (((sraw > 0) && (sraw < 65000))) {
if ((sraw < 20001)) {
sraw = 20001;
} else if ((sraw > 52767)) {
sraw = 52767;
}
params->mSraw = (fix16_from_int((sraw - 20000)));
}
params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw);
params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index);
params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index);
if ((params->mVoc_Index < F16(0.5))) {
params->mVoc_Index = F16(0.5);
}
if ((params->mSraw > F16(0.))) {
voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index);
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
voc_algorithm_mean_variance_estimator_get_mean(params));
}
}
*voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5))));
}
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.));
voc_algorithm_mean_variance_estimator_init_instances(params);
}
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_sigmoid_init(params);
}
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
fix16_t tau_mean_variance_hours,
fix16_t gating_max_duration_minutes) {
params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes;
params->m_Mean_Variance_Estimator_Initialized = false;
params->m_Mean_Variance_Estimator_Mean = F16(0.);
params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.);
params->m_Mean_Variance_Estimator_Std = std_initial;
params->m_Mean_Variance_Estimator_Gamma =
(fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))),
(tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.)))));
params->m_Mean_Variance_Estimator_Gamma_Initial_Mean =
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
(VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Mean_Variance_Estimator_Gamma_Initial_Variance =
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
(VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.);
params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.);
params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.);
params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
}
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
fix16_t uptime_gamma) {
params->m_Mean_Variance_Estimator_Mean = mean;
params->m_Mean_Variance_Estimator_Std = std;
params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma;
params->m_Mean_Variance_Estimator_Initialized = true;
}
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) {
return params->m_Mean_Variance_Estimator_Std;
}
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) {
return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset);
}
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
fix16_t voc_index_from_prior) {
fix16_t uptime_limit;
fix16_t sigmoid_gamma_mean;
fix16_t gamma_mean;
fix16_t gating_threshold_mean;
fix16_t sigmoid_gating_mean;
fix16_t sigmoid_gamma_variance;
fix16_t gamma_variance;
fix16_t gating_threshold_variance;
fix16_t sigmoid_gating_variance;
uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL));
if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) {
params->m_Mean_Variance_Estimator_Uptime_Gamma =
(params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
}
if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) {
params->m_Mean_Variance_Estimator_Uptime_Gating =
(params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
}
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN),
F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN));
sigmoid_gamma_mean =
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
gamma_mean =
(params->m_Mean_Variance_Estimator_Gamma +
(fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma),
sigmoid_gamma_mean)));
gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) +
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
voc_algorithm_mean_variance_estimator_sigmoid_process(
params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean,
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(
params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE));
sigmoid_gamma_variance =
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
gamma_variance =
(params->m_Mean_Variance_Estimator_Gamma +
(fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma),
(sigmoid_gamma_variance - sigmoid_gamma_mean))));
gating_threshold_variance =
(F16(VOC_ALGORITHM_GATING_THRESHOLD) +
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
voc_algorithm_mean_variance_estimator_sigmoid_process(
params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance,
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance));
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes =
(params->m_Mean_Variance_Estimator_Gating_Duration_Minutes +
(fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)),
((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) -
F16(VOC_ALGORITHM_GATING_MAX_RATIO)))));
if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) {
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
}
if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes >
params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) {
params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
}
}
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
fix16_t voc_index_from_prior) {
fix16_t delta_sgp;
fix16_t c;
fix16_t additional_scaling;
if ((!params->m_Mean_Variance_Estimator_Initialized)) {
params->m_Mean_Variance_Estimator_Initialized = true;
params->m_Mean_Variance_Estimator_Sraw_Offset = sraw;
params->m_Mean_Variance_Estimator_Mean = F16(0.);
} else {
if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) ||
(params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) {
params->m_Mean_Variance_Estimator_Sraw_Offset =
(params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean);
params->m_Mean_Variance_Estimator_Mean = F16(0.);
}
sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset);
voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior);
delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean),
F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING)));
if ((delta_sgp < F16(0.))) {
c = (params->m_Mean_Variance_Estimator_Std - delta_sgp);
} else {
c = (params->m_Mean_Variance_Estimator_Std + delta_sgp);
}
additional_scaling = F16(1.);
if ((c > F16(1440.))) {
additional_scaling = F16(4.);
}
params->m_Mean_Variance_Estimator_Std = (fix16_mul(
fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) -
params->m_Mean_Variance_Estimator_Gamma_Variance)))),
fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std,
(fix16_div(params->m_Mean_Variance_Estimator_Std,
(fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING),
additional_scaling)))))) +
(fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)),
additional_scaling)),
delta_sgp))))));
params->m_Mean_Variance_Estimator_Mean =
(params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp)));
}
}
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.));
}
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
fix16_t x0, fix16_t k) {
params->m_Mean_Variance_Estimator_Sigmoid_L = l;
params->m_Mean_Variance_Estimator_Sigmoid_K = k;
params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0;
}
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t x;
x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0)));
if ((x < F16(-50.))) {
return params->m_Mean_Variance_Estimator_Sigmoid_L;
} else if ((x > F16(50.))) {
return F16(0.);
} else {
return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x))));
}
}
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) {
voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.));
}
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) {
params->m_Mox_Model_Sraw_Std = sraw_std;
params->m_Mox_Model_Sraw_Mean = sraw_mean;
}
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) {
return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean),
(-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))),
F16(VOC_ALGORITHM_VOC_INDEX_GAIN)));
}
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) {
voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.));
}
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) {
params->m_Sigmoid_Scaled_Offset = offset;
}
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t x;
fix16_t shift;
x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0))));
if ((x < F16(-50.))) {
return F16(VOC_ALGORITHM_SIGMOID_L);
} else if ((x > F16(50.))) {
return F16(0.);
} else {
if ((sample >= F16(0.))) {
shift =
(fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.)));
return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift);
} else {
return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))),
(fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x))))));
}
}
}
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) {
voc_algorithm_adaptive_lowpass_set_parameters(params);
}
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) {
params->m_Adaptive_Lowpass_A1 =
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Adaptive_Lowpass_A2 =
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Adaptive_Lowpass_Initialized = false;
}
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t abs_delta;
fix16_t f1;
fix16_t tau_a;
fix16_t a3;
if ((!params->m_Adaptive_Lowpass_Initialized)) {
params->m_Adaptive_Lowpass_X1 = sample;
params->m_Adaptive_Lowpass_X2 = sample;
params->m_Adaptive_Lowpass_X3 = sample;
params->m_Adaptive_Lowpass_Initialized = true;
}
params->m_Adaptive_Lowpass_X1 =
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) +
(fix16_mul(params->m_Adaptive_Lowpass_A1, sample)));
params->m_Adaptive_Lowpass_X2 =
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) +
(fix16_mul(params->m_Adaptive_Lowpass_A2, sample)));
abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2);
if ((abs_delta < F16(0.))) {
abs_delta = (-abs_delta);
}
f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta)));
tau_a =
((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST));
a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a)));
params->m_Adaptive_Lowpass_X3 =
((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample)));
return params->m_Adaptive_Lowpass_X3;
}
} // namespace sgp40
} // namespace esphome

View File

@ -1,147 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace sgp40 {
/* The VOC code were originally created by
* https://github.com/Sensirion/embedded-sgp
* The fixed point arithmetic parts of this code were originally created by
* https://github.com/PetteriAimonen/libfixmath
*/
using fix16_t = int32_t;
#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5)))
static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.);
static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.);
static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.);
static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.);
static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.);
static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.);
static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.);
static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75));
static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01);
static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.);
static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45));
static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01);
static const float VOC_ALGORITHM_GATING_THRESHOLD(340.);
static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.);
static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09);
static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.));
static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3);
static const float VOC_ALGORITHM_SIGMOID_L(500.);
static const float VOC_ALGORITHM_SIGMOID_K(-0.0065);
static const float VOC_ALGORITHM_SIGMOID_X0(213.);
static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.);
static const float VOC_ALGORITHM_LP_TAU_FAST(20.0);
static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0);
static const float VOC_ALGORITHM_LP_ALPHA(-0.2);
static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.));
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.);
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.);
/**
* Struct to hold all the states of the VOC algorithm.
*/
struct VocAlgorithmParams {
fix16_t mVoc_Index_Offset;
fix16_t mTau_Mean_Variance_Hours;
fix16_t mGating_Max_Duration_Minutes;
fix16_t mSraw_Std_Initial;
fix16_t mUptime;
fix16_t mSraw;
fix16_t mVoc_Index;
fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes;
bool m_Mean_Variance_Estimator_Initialized;
fix16_t m_Mean_Variance_Estimator_Mean;
fix16_t m_Mean_Variance_Estimator_Sraw_Offset;
fix16_t m_Mean_Variance_Estimator_Std;
fix16_t m_Mean_Variance_Estimator_Gamma;
fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean;
fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance;
fix16_t m_Mean_Variance_Estimator_Gamma_Mean;
fix16_t m_Mean_Variance_Estimator_Gamma_Variance;
fix16_t m_Mean_Variance_Estimator_Uptime_Gamma;
fix16_t m_Mean_Variance_Estimator_Uptime_Gating;
fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes;
fix16_t m_Mean_Variance_Estimator_Sigmoid_L;
fix16_t m_Mean_Variance_Estimator_Sigmoid_K;
fix16_t m_Mean_Variance_Estimator_Sigmoid_X0;
fix16_t m_Mox_Model_Sraw_Std;
fix16_t m_Mox_Model_Sraw_Mean;
fix16_t m_Sigmoid_Scaled_Offset;
fix16_t m_Adaptive_Lowpass_A1;
fix16_t m_Adaptive_Lowpass_A2;
bool m_Adaptive_Lowpass_Initialized;
fix16_t m_Adaptive_Lowpass_X1;
fix16_t m_Adaptive_Lowpass_X2;
fix16_t m_Adaptive_Lowpass_X3;
};
/**
* Initialize the VOC algorithm parameters. Call this once at the beginning or
* whenever the sensor stopped measurements.
* @param params Pointer to the VocAlgorithmParams struct
*/
void voc_algorithm_init(VocAlgorithmParams *params);
/**
* Get current algorithm states. Retrieved values can be used in
* voc_algorithm_set_states() to resume operation after a short interruption,
* skipping initial learning phase. This feature can only be used after at least
* 3 hours of continuous operation.
* @param params Pointer to the VocAlgorithmParams struct
* @param state0 State0 to be stored
* @param state1 State1 to be stored
*/
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1);
/**
* Set previously retrieved algorithm states to resume operation after a short
* interruption, skipping initial learning phase. This feature should not be
* used after inerruptions of more than 10 minutes. Call this once after
* voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if
* desired. Otherwise, the algorithm will start with initial learning phase.
* @param params Pointer to the VocAlgorithmParams struct
* @param state0 State0 to be restored
* @param state1 State1 to be restored
*/
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1);
/**
* Set parameters to customize the VOC algorithm. Call this once after
* voc_algorithm_init(), if desired. Otherwise, the default values will be used.
*
* @param params Pointer to the VocAlgorithmParams struct
* @param voc_index_offset VOC index representing typical (average)
* conditions. Range 1..250, default 100
* @param learning_time_hours Time constant of long-term estimator.
* Past events will be forgotten after about
* twice the learning time.
* Range 1..72 [hours], default 12 [hours]
* @param gating_max_duration_minutes Maximum duration of gating (freeze of
* estimator during high VOC index signal).
* 0 (no gating) or range 1..720 [minutes],
* default 180 [minutes]
* @param std_initial Initial estimate for standard deviation.
* Lower value boosts events during initial
* learning period, but may result in larger
* device-to-device variations.
* Range 10..500, default 50
*/
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
int32_t std_initial);
/**
* Calculate the VOC index value from the raw sensor value.
*
* @param params Pointer to the VocAlgorithmParams struct
* @param sraw Raw value from the SGP40 sensor
* @param voc_index Calculated VOC index value from the raw sensor value. Zero
* during initial blackout period and 1..500 afterwards
*/
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index);
} // namespace sgp40
} // namespace esphome

View File

@ -1,70 +1,8 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome.const import (
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw"]
sgp40_ns = cg.esphome_ns.namespace("sgp40")
SGP40Component = sgp40_ns.class_(
"SGP40Component",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
"SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n"
" See https://esphome.io/components/sensor/sgp4x.html"
)
CONF_COMPENSATION = "compensation"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_VOC_BASELINE = "voc_baseline"
CONFIG_SCHEMA = (
sensor.sensor_schema(
SGP40Component,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
},
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x59))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if CONF_VOC_BASELINE in config:
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))

View File

@ -1,274 +0,0 @@
#include "sgp40.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sgp40 {
static const char *const TAG = "sgp40";
void SGP40Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP40...");
// Serial Number identification
if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
uint16_t raw_serial_number[3];
if (!this->read_data(raw_serial_number, 3)) {
this->mark_failed();
return;
}
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
(uint64_t(raw_serial_number[2]));
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use
if (!this->write_command(SGP40_CMD_GET_FEATURESET)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed();
return;
}
uint16_t raw_featureset;
if (!this->read_data(raw_featureset)) {
ESP_LOGD(TAG, "raw_featureset read_data_ failed");
this->mark_failed();
return;
}
this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
voc_algorithm_init(&this->voc_algorithm_params_);
if (this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<SGP40Baselines>(hash, true);
if (this->pref_.load(&this->baselines_storage_)) {
this->state0_ = this->baselines_storage_.state0;
this->state1_ = this->baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0,
this->baselines_storage_.state1);
}
}
this->self_test_();
/* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf
indicates this sensor should be driven at 1Hz. Comments from the developers at:
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit
resilient to slight timing variations so the software timer should be accurate enough for
this.
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
to the update method. This seperation is to support getting accurate measurements but
limit the amount of communication done over wifi for power consumption or to keep the
number of records reported from being overwhelming.
*/
ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
this->set_interval(1000, [this]() { this->update_voc_index(); });
}
void SGP40Component::self_test_() {
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command(SGP40_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(250, [this]() {
uint16_t reply;
if (!this->read_data(reply)) {
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply == 0xD400) {
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
}
ESP_LOGD(TAG, "Self-test failed");
this->mark_failed();
});
}
/**
* @brief Combined the measured gasses, temperature, and humidity
* to calculate the VOC Index
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return int32_t The VOC Index
*/
int32_t SGP40Component::measure_voc_index_() {
int32_t voc_index;
uint16_t sraw = measure_raw_();
if (sraw == UINT16_MAX)
return UINT16_MAX;
this->status_clear_warning();
voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index);
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_);
if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF ||
(uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->baselines_storage_.state0 = this->state0_;
this->baselines_storage_.state1 = this->state1_;
if (this->pref_.save(&this->baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
return voc_index;
}
/**
* @brief Return the raw gas measurement
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return uint16_t The current raw gas measurement
*/
uint16_t SGP40Component::measure_raw_() {
float humidity = NAN;
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return UINT16_MAX;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
humidity = 50;
}
float temperature = NAN;
if (this->temperature_sensor_ != nullptr) {
temperature = float(this->temperature_sensor_->state);
}
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
temperature = 25;
}
uint16_t data[2];
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
// first paramater is the relative humidity ticks
data[0] = rhticks;
// second paramater is the temperature ticks
data[1] = tempticks;
if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return false;
}
delay(30);
uint16_t raw_data;
if (!this->read_data(raw_data)) {
this->status_set_warning();
ESP_LOGD(TAG, "read_data_ error");
return UINT16_MAX;
}
return raw_data;
}
void SGP40Component::update_voc_index() {
this->seconds_since_last_store_ += 1;
this->voc_index_ = this->measure_voc_index_();
if (this->samples_read_ < this->samples_to_stabalize_) {
this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
this->samples_to_stabalize_, this->voc_index_);
return;
}
}
void SGP40Component::update() {
if (this->samples_read_ < this->samples_to_stabalize_) {
return;
}
if (this->voc_index_ != UINT16_MAX) {
this->status_clear_warning();
this->publish_state(this->voc_index_);
} else {
this->status_set_warning();
}
}
void SGP40Component::dump_config() {
ESP_LOGCONFIG(TAG, "SGP40:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
} else {
ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Compensation:");
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
}
}
} // namespace sgp40
} // namespace esphome

View File

@ -1,93 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
#include "sensirion_voc_algorithm.h"
#include <cmath>
namespace esphome {
namespace sgp40 {
struct SGP40Baselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
// commands and constants
static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library
static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial
static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC
static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word
// Commands
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP40Component;
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
public:
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void setup() override;
void update() override;
void update_voc_index();
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
protected:
/// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
int16_t sensirion_init_sensors_();
int16_t sgp40_probe_();
uint64_t serial_number_;
uint16_t featureset_;
int32_t measure_voc_index_();
uint8_t generate_crc_(const uint8_t *data, uint8_t datalen);
uint16_t measure_raw_();
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP40Baselines baselines_storage_;
VocAlgorithmParams voc_algorithm_params_;
bool self_test_complete_;
bool store_baseline_;
int32_t state0_;
int32_t state1_;
int32_t voc_index_ = 0;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabalize_ = static_cast<int8_t>(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2;
/**
* @brief Request the sensor to perform a self-test, returning the result
*
* @return true: success false:failure
*/
void self_test_();
enum ErrorCode {
COMMUNICATION_FAILED,
MEASUREMENT_INIT_FAILED,
INVALID_ID,
UNSUPPORTED_ID,
UNKNOWN
} error_code_{UNKNOWN};
};
} // namespace sgp40
} // namespace esphome

View File

View File

@ -0,0 +1,144 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome.const import (
CONF_ID,
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw", "@martgras"]
sgp4x_ns = cg.esphome_ns.namespace("sgp4x")
SGP4xComponent = sgp4x_ns.class_(
"SGP4xComponent",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
)
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_COMPENSATION = "compensation"
CONF_GAIN_FACTOR = "gain_factor"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
def validate_sensors(config):
if CONF_VOC not in config and CONF_NOX not in config:
raise cv.Invalid(
f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}"
)
return config
GAS_SENSOR = cv.Schema(
{
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
{
cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_,
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_,
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_,
cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_,
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_,
}
)
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SGP4xComponent),
cv.Optional(CONF_VOC): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_NOX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_NITROUS_OXIDE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
},
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x59)),
validate_sensors,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if CONF_VOC_BASELINE in config:
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))
if CONF_VOC in config:
sens = await sensor.new_sensor(config[CONF_VOC])
cg.add(var.set_voc_sensor(sens))
if CONF_ALGORITHM_TUNING in config[CONF_VOC]:
cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
cg.add(
var.set_voc_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_STD_INITIAL],
cfg[CONF_GAIN_FACTOR],
)
)
if CONF_NOX in config:
sens = await sensor.new_sensor(config[CONF_NOX])
cg.add(var.set_nox_sensor(sens))
if CONF_ALGORITHM_TUNING in config[CONF_NOX]:
cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
cg.add(
var.set_nox_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_GAIN_FACTOR],
)
)
cg.add_library(
None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git"
)

View File

@ -0,0 +1,343 @@
#include "sgp4x.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sgp4x {
static const char *const TAG = "sgp4x";
void SGP4xComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP4x...");
// Serial Number identification
uint16_t raw_serial_number[3];
if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) {
ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed();
return;
}
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
(uint64_t(raw_serial_number[2]));
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use
uint16_t raw_featureset;
if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed();
return;
}
this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) {
sgp_type_ = SGP40;
self_test_time_ = SPG40_SELFTEST_TIME;
measure_time_ = SGP40_MEASURE_TIME;
if (this->nox_sensor_) {
ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected");
// disable the sensor
this->nox_sensor_->set_disabled_by_default(true);
// make sure it's not visiable in HA
this->nox_sensor_->set_internal(true);
this->nox_sensor_->state = NAN;
// remove pointer to sensor
this->nox_sensor_ = nullptr;
}
} else {
if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) {
sgp_type_ = SGP41;
self_test_time_ = SPG41_SELFTEST_TIME;
measure_time_ = SGP41_MEASURE_TIME;
} else {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET);
this->mark_failed();
return;
}
}
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
if (this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<SGP4xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
this->voc_state0_ = this->voc_baselines_storage_.state0;
this->voc_state1_ = this->voc_baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
}
if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
voc_algorithm_.set_tuning_parameters(
voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
}
if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
nox_algorithm_.set_tuning_parameters(
nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
}
this->self_test_();
/* The official spec for this sensor at
https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
sensor should be driven at 1Hz. Comments from the developers at:
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
timing variations so the software timer should be accurate enough for this.
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
to the update method. This seperation is to support getting accurate measurements but
limit the amount of communication done over wifi for power consumption or to keep the
number of records reported from being overwhelming.
*/
ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
this->set_interval(1000, [this]() { this->update_gas_indices(); });
}
void SGP4xComponent::self_test_() {
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(self_test_time_, [this]() {
uint16_t reply;
if (!this->read_data(reply)) {
this->error_code_ = SELF_TEST_FAILED;
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply == 0xD400) {
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
} else {
this->error_code_ = SELF_TEST_FAILED;
ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
return;
}
ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
this->mark_failed();
});
}
/**
* @brief Combined the measured gasses, temperature, and humidity
* to calculate the VOC Index
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return int32_t The VOC Index
*/
bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
uint16_t voc_sraw;
uint16_t nox_sraw;
if (!measure_raw_(voc_sraw, nox_sraw))
return false;
this->status_clear_warning();
voc = voc_algorithm_.process(voc_sraw);
if (nox_sensor_) {
nox = nox_algorithm_.process(nox_sraw);
}
ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox);
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
(uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = this->voc_state0_;
this->voc_baselines_storage_.state1 = this->voc_state1_;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
return true;
}
/**
* @brief Return the raw gas measurement
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return uint16_t The current raw gas measurement
*/
bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
float humidity = NAN;
static uint32_t nox_conditioning_start = millis();
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return false;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
humidity = 50;
}
float temperature = NAN;
if (this->temperature_sensor_ != nullptr) {
temperature = float(this->temperature_sensor_->state);
}
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
temperature = 25;
}
uint16_t command;
uint16_t data[2];
size_t response_words;
// Use SGP40 measure command if we don't care about NOx
if (nox_sensor_ == nullptr) {
command = SGP40_CMD_MEASURE_RAW;
response_words = 1;
} else {
// SGP41 sensor must use NOx conditioning command for the first 10 seconds
if (millis() - nox_conditioning_start < 10000) {
command = SGP41_CMD_NOX_CONDITIONING;
response_words = 1;
} else {
command = SGP41_CMD_MEASURE_RAW;
response_words = 2;
}
}
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
// first paramater are the relative humidity ticks
data[0] = rhticks;
// secomd paramater are the temperature ticks
data[1] = tempticks;
if (!this->write_command(command, data, 2)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return false;
}
delay(measure_time_);
uint16_t raw_data[2];
raw_data[1] = 0;
if (!this->read_data(raw_data, response_words)) {
this->status_set_warning();
ESP_LOGD(TAG, "read error (%d)", this->last_error_);
return false;
}
voc_raw = raw_data[0];
nox_raw = raw_data[1]; // either 0 or the measured NOx ticks
return true;
}
void SGP4xComponent::update_gas_indices() {
if (!this->self_test_complete_)
return;
this->seconds_since_last_store_ += 1;
if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
// Set values to UINT16_MAX to indicate failure
this->voc_index_ = this->nox_index_ = UINT16_MAX;
ESP_LOGE(TAG, "measure gas indices failed");
return;
}
if (this->samples_read_ < this->samples_to_stabilize_) {
this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
this->samples_to_stabilize_, this->voc_index_);
return;
}
}
void SGP4xComponent::update() {
if (this->samples_read_ < this->samples_to_stabilize_) {
return;
}
if (this->voc_sensor_) {
if (this->voc_index_ != UINT16_MAX) {
this->status_clear_warning();
this->voc_sensor_->publish_state(this->voc_index_);
} else {
this->status_set_warning();
}
}
if (this->nox_sensor_) {
if (this->nox_index_ != UINT16_MAX) {
this->status_clear_warning();
this->nox_sensor_->publish_state(this->nox_index_);
} else {
this->status_set_warning();
}
}
}
void SGP4xComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SGP4x:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case SERIAL_NUMBER_IDENTIFICATION_FAILED:
ESP_LOGW(TAG, "Get Serial number failed.");
break;
case SELF_TEST_FAILED:
ESP_LOGW(TAG, "Self test failed.");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
} else {
ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Compensation:");
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
}
LOG_SENSOR(" ", "VOC", this->voc_sensor_);
LOG_SENSOR(" ", "NOx", this->nox_sensor_);
}
} // namespace sgp4x
} // namespace esphome

View File

@ -0,0 +1,142 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
#include <VOCGasIndexAlgorithm.h>
#include <NOxGasIndexAlgorithm.h>
#include <cmath>
namespace esphome {
namespace sgp4x {
struct SGP4xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
enum SgpType { SGP40, SGP41 };
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
uint16_t learning_time_gain_hours;
uint16_t gating_max_duration_minutes;
uint16_t std_initial;
uint16_t gain_factor;
};
// commands and constants
static const uint8_t SGP40_FEATURESET = 0x0020; // can measure VOC
static const uint8_t SGP41_FEATURESET = 0x0040; // can measure VOC and NOX
// Commands
static const uint16_t SGP4X_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP4X_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619;
static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612;
static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12;
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
static const uint16_t SPG40_SELFTEST_TIME = 250; // 250 ms for self test
static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test
static const uint16_t SGP40_MEASURE_TIME = 30;
static const uint16_t SGP41_MEASURE_TIME = 55;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP4xComponent;
/// This class implements support for the Sensirion sgp4x i2c GAS (VOC) sensors.
class SGP4xComponent : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
enum ErrorCode {
COMMUNICATION_FAILED,
MEASUREMENT_INIT_FAILED,
INVALID_ID,
UNSUPPORTED_ID,
SERIAL_NUMBER_IDENTIFICATION_FAILED,
SELF_TEST_FAILED,
UNKNOWN
} error_code_{UNKNOWN};
public:
// SGP4xComponent() {};
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void setup() override;
void update() override;
void update_gas_indices();
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
voc_tuning_params_.value().index_offset = index_offset;
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
voc_tuning_params_.value().std_initial = std_initial;
voc_tuning_params_.value().gain_factor = gain_factor;
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) {
nox_tuning_params_.value().index_offset = index_offset;
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
nox_tuning_params_.value().std_initial = 50;
nox_tuning_params_.value().gain_factor = gain_factor;
}
protected:
void self_test_();
/// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
int16_t sensirion_init_sensors_();
bool measure_gas_indices_(int32_t &voc, int32_t &nox);
bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw);
SgpType sgp_type_{SGP40};
uint64_t serial_number_;
uint16_t featureset_;
bool self_test_complete_;
uint16_t self_test_time_;
sensor::Sensor *voc_sensor_{nullptr};
VOCGasIndexAlgorithm voc_algorithm_;
optional<GasTuning> voc_tuning_params_;
int32_t voc_state0_;
int32_t voc_state1_;
int32_t voc_index_ = 0;
sensor::Sensor *nox_sensor_{nullptr};
int32_t nox_index_ = 0;
NOxGasIndexAlgorithm nox_algorithm_;
optional<GasTuning> nox_tuning_params_;
uint16_t measure_time_;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2;
bool store_baseline_;
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP4xBaselines voc_baselines_storage_;
};
} // namespace sgp4x
} // namespace esphome

View File

@ -41,7 +41,6 @@
* O FF FF FF FF FF FF FF FF - Not used
* M 6C - CRC over bytes 2 to F (Addition)
\*********************************************************************************************/
#include <cmath>
#include "sonoff_d1.h"
namespace esphome {
@ -263,7 +262,7 @@ void SonoffD1Output::write_state(light::LightState *state) {
state->current_values_as_brightness(&brightness);
// Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100)
const uint8_t calculated_brightness = std::round(brightness * 100);
const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100);
if (calculated_brightness == 0) {
// if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness");

View File

@ -1,5 +1,4 @@
#include "speed_fan.h"
#include "esphome/components/fan/fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {

View File

@ -48,6 +48,8 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@ -30,6 +30,8 @@ class SSD1322 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
protected:
virtual void command(uint8_t value) = 0;
virtual void data(uint8_t value) = 0;

View File

@ -35,6 +35,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@ -30,6 +30,8 @@ class SSD1327 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@ -25,6 +25,8 @@ class SSD1331 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
virtual void command(uint8_t value) = 0;
virtual void write_display_data() = 0;

View File

@ -31,6 +31,8 @@ class SSD1351 : public PollingComponent, public display::DisplayBuffer {
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void fill(Color color) override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
virtual void command(uint8_t value) = 0;
virtual void data(uint8_t value) = 0;

View File

@ -53,6 +53,8 @@ class ST7735 : public PollingComponent,
void set_dc_pin(GPIOPin *value) { dc_pin_ = value; }
size_t get_buffer_length();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes);
void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes);

View File

@ -126,6 +126,8 @@ class ST7789V : public PollingComponent,
void write_display_data();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
protected:
GPIOPin *dc_pin_;
GPIOPin *reset_pin_{nullptr};

View File

@ -29,6 +29,8 @@ class ST7920 : public PollingComponent,
void fill(Color color) override;
void write_display_data();
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_height_internal() override;

View File

@ -48,7 +48,7 @@ VARIABLE_PROG = re.compile(
)
def _expand_substitutions(substitutions, value, path):
def _expand_substitutions(substitutions, value, path, ignore_missing):
if "$" not in value:
return value
@ -66,13 +66,14 @@ def _expand_substitutions(substitutions, value, path):
if name.startswith("{") and name.endswith("}"):
name = name[1:-1]
if name not in substitutions:
_LOGGER.warning(
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
"not declared",
orig_value,
"->".join(str(x) for x in path),
name,
)
if not ignore_missing:
_LOGGER.warning(
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
"not declared",
orig_value,
"->".join(str(x) for x in path),
name,
)
i = j
continue
@ -92,37 +93,37 @@ def _expand_substitutions(substitutions, value, path):
return value
def _substitute_item(substitutions, item, path):
def _substitute_item(substitutions, item, path, ignore_missing):
if isinstance(item, list):
for i, it in enumerate(item):
sub = _substitute_item(substitutions, it, path + [i])
sub = _substitute_item(substitutions, it, path + [i], ignore_missing)
if sub is not None:
item[i] = sub
elif isinstance(item, dict):
replace_keys = []
for k, v in item.items():
if path or k != CONF_SUBSTITUTIONS:
sub = _substitute_item(substitutions, k, path + [k])
sub = _substitute_item(substitutions, k, path + [k], ignore_missing)
if sub is not None:
replace_keys.append((k, sub))
sub = _substitute_item(substitutions, v, path + [k])
sub = _substitute_item(substitutions, v, path + [k], ignore_missing)
if sub is not None:
item[k] = sub
for old, new in replace_keys:
item[new] = merge_config(item.get(old), item.get(new))
del item[old]
elif isinstance(item, str):
sub = _expand_substitutions(substitutions, item, path)
sub = _expand_substitutions(substitutions, item, path, ignore_missing)
if sub != item:
return sub
elif isinstance(item, core.Lambda):
sub = _expand_substitutions(substitutions, item.value, path)
sub = _expand_substitutions(substitutions, item.value, path, ignore_missing)
if sub != item:
item.value = sub
return None
def do_substitution_pass(config, command_line_substitutions):
def do_substitution_pass(config, command_line_substitutions, ignore_missing=False):
if CONF_SUBSTITUTIONS not in config and not command_line_substitutions:
return
@ -151,4 +152,4 @@ def do_substitution_pass(config, command_line_substitutions):
config[CONF_SUBSTITUTIONS] = substitutions
# Move substitutions to the first place to replace substitutions in them correctly
config.move_to_end(CONF_SUBSTITUTIONS, False)
_substitute_item(substitutions, config, [])
_substitute_item(substitutions, config, [], ignore_missing)

View File

@ -43,7 +43,6 @@ void Switch::add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; }
uint32_t Switch::hash_base() { return 3129890955UL; }
bool Switch::is_inverted() const { return this->inverted_; }
std::string Switch::get_device_class() {

View File

@ -107,8 +107,6 @@ class Switch : public EntityBase {
*/
virtual void write_state(bool state) = 0;
uint32_t hash_base() override;
CallbackManager<void(bool)> state_callback_{};
bool inverted_{false};
Deduplicator<bool> publish_dedup_;

View File

@ -31,6 +31,7 @@ TCS34725Component = tcs34725_ns.class_(
TCS34725IntegrationTime = tcs34725_ns.enum("TCS34725IntegrationTime")
TCS34725_INTEGRATION_TIMES = {
"auto": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_AUTO,
"2.4ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS,
"24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS,
"50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS,
@ -88,7 +89,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema,
cv.Optional(CONF_ILLUMINANCE): illuminance_schema,
cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema,
cv.Optional(CONF_INTEGRATION_TIME, default="2.4ms"): cv.enum(
cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum(
TCS34725_INTEGRATION_TIMES, lower=True
),
cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True),

View File

@ -136,8 +136,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
}
/* Check for saturation and mark the sample as invalid if true */
if (c >= sat) {
ESP_LOGW(TAG, "Saturation too high, discarding sample with saturation %.1f and clear %d", sat, c);
return;
if (this->integration_time_auto_) {
ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing");
} else {
ESP_LOGW(
TAG,
"Saturation too high, sample with saturation %.1f and clear %d treat values carefully or use grey filter",
sat, c);
}
}
/* AMS RGB sensors have no IR channel, so the IR content must be */
@ -149,8 +155,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u
g2 = g - ir;
b2 = b - ir;
// discarding super low values? not recemmonded, and avoided by using auto gain.
if (r2 == 0) {
return;
// legacy code
if (!this->integration_time_auto_) {
ESP_LOGW(TAG,
"No light detected on red channel, switch to auto gain or adjust timing, values will be unreliable");
return;
}
}
// Lux Calculation (DN40 3.2)
@ -189,7 +201,7 @@ void TCS34725Component::update() {
this->status_set_warning();
return;
}
ESP_LOGV(TAG, "Raw values clear=%x red=%x green=%x blue=%x", raw_c, raw_r, raw_g, raw_b);
ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b);
float channel_c;
float channel_r;
@ -220,20 +232,95 @@ void TCS34725Component::update() {
calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c);
}
if (this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(this->illuminance_);
// do not publish values if auto gain finding ongoing, and oversaturated
// so: publish when:
// - not auto mode
// - clear not oversaturated
// - clear oversaturated but gain and timing cannot go lower
if (!this->integration_time_auto_ || raw_c < 65530 || (this->gain_reg_ == 0 && this->integration_time_ < 200)) {
if (this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(this->illuminance_);
if (this->color_temperature_sensor_ != nullptr)
this->color_temperature_sensor_->publish_state(this->color_temperature_);
if (this->color_temperature_sensor_ != nullptr)
this->color_temperature_sensor_->publish_state(this->color_temperature_);
}
ESP_LOGD(TAG, "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color Temperature=%.1fK",
ESP_LOGD(TAG,
"Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color "
"Temperature=%.1fK",
channel_r, channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_);
if (this->integration_time_auto_) {
// change integration time an gain to achieve maximum resolution an dynamic range
// calculate optimal integration time to achieve 70% satuaration
float integration_time_ideal;
integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_;
uint8_t gain_reg_val_new = this->gain_reg_;
// increase gain if less than 20% of white channel used and high integration time
// increase only if not already maximum
// do not use max gain, as ist will not get better
if (this->gain_reg_ < 3) {
if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) {
gain_reg_val_new = this->gain_reg_ + 1;
// update integration time to new situation
integration_time_ideal = integration_time_ideal / 4;
}
}
// decrease gain, if very high clear values and integration times alreadey low
if (this->gain_reg_ > 0) {
if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) {
gain_reg_val_new = this->gain_reg_ - 1;
// update integration time to new situation
integration_time_ideal = integration_time_ideal * 4;
}
}
// saturate integration times
float integration_time_next = integration_time_ideal;
if (integration_time_ideal > 2.4f * 256) {
integration_time_next = 2.4f * 256;
}
if (integration_time_ideal < 154) {
integration_time_next = 154;
}
// calculate register value from timing
uint8_t regval_atime = (uint8_t)(256.f - integration_time_next / 2.4f);
ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d gain reg: %d",
this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_);
if (this->integration_reg_ != regval_atime || gain_reg_val_new != this->gain_reg_) {
this->integration_reg_ = regval_atime;
this->gain_reg_ = gain_reg_val_new;
set_gain((TCS34725Gain) gain_reg_val_new);
if (this->write_config_register_(TCS34725_REGISTER_ATIME, this->integration_reg_) != i2c::ERROR_OK ||
this->write_config_register_(TCS34725_REGISTER_CONTROL, this->gain_reg_) != i2c::ERROR_OK) {
this->mark_failed();
ESP_LOGW(TAG, "TCS34725I update timing failed!");
} else {
this->integration_time_ = integration_time_next;
}
}
}
this->status_clear_warning();
}
void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) {
this->integration_reg_ = integration_time;
this->integration_time_ = (256.f - integration_time) * 2.4f;
// if an integration time is 0x100, this is auto start with 154ms as this gives best starting point
TCS34725IntegrationTime my_integration_time_regval;
if (integration_time == TCS34725_INTEGRATION_TIME_AUTO) {
this->integration_time_auto_ = true;
this->integration_reg_ = TCS34725_INTEGRATION_TIME_154MS;
my_integration_time_regval = TCS34725_INTEGRATION_TIME_154MS;
} else {
this->integration_reg_ = integration_time;
my_integration_time_regval = integration_time;
this->integration_time_auto_ = false;
}
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
}
void TCS34725Component::set_gain(TCS34725Gain gain) {
this->gain_reg_ = gain;

View File

@ -26,6 +26,7 @@ enum TCS34725IntegrationTime {
TCS34725_INTEGRATION_TIME_540MS = 0x1F,
TCS34725_INTEGRATION_TIME_600MS = 0x06,
TCS34725_INTEGRATION_TIME_614MS = 0x00,
TCS34725_INTEGRATION_TIME_AUTO = 0x100,
};
enum TCS34725Gain {
@ -77,10 +78,11 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
float glass_attenuation_{1.0};
float illuminance_;
float color_temperature_;
bool integration_time_auto_{true};
private:
void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c);
uint8_t integration_reg_{TCS34725_INTEGRATION_TIME_2_4MS};
uint16_t integration_reg_;
uint8_t gain_reg_{TCS34725_GAIN_1X};
};

View File

@ -70,7 +70,6 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
std::string TextSensor::unique_id() { return ""; }
bool TextSensor::has_state() { return this->has_state_; }
uint32_t TextSensor::hash_base() { return 334300109UL; }
} // namespace text_sensor
} // namespace esphome

View File

@ -59,8 +59,6 @@ class TextSensor : public EntityBase {
void internal_send_state_to_frontend(const std::string &state);
protected:
uint32_t hash_base() override;
CallbackManager<void(std::string)> raw_callback_; ///< Storage for raw state callbacks.
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.

View File

@ -14,6 +14,7 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_DRY_ACTION,
CONF_DRY_MODE,
CONF_FAN_MODE,
CONF_FAN_MODE_ON_ACTION,
CONF_FAN_MODE_OFF_ACTION,
CONF_FAN_MODE_AUTO_ACTION,
@ -37,6 +38,7 @@ from esphome.const import (
CONF_IDLE_ACTION,
CONF_MAX_COOLING_RUN_TIME,
CONF_MAX_HEATING_RUN_TIME,
CONF_MAX_TEMPERATURE,
CONF_MIN_COOLING_OFF_TIME,
CONF_MIN_COOLING_RUN_TIME,
CONF_MIN_FAN_MODE_SWITCHING_TIME,
@ -45,7 +47,11 @@ from esphome.const import (
CONF_MIN_HEATING_OFF_TIME,
CONF_MIN_HEATING_RUN_TIME,
CONF_MIN_IDLE_TIME,
CONF_MIN_TEMPERATURE,
CONF_NAME,
CONF_MODE,
CONF_OFF_MODE,
CONF_PRESET,
CONF_SENSOR,
CONF_SET_POINT_MINIMUM_DIFFERENTIAL,
CONF_STARTUP_DELAY,
@ -55,11 +61,15 @@ from esphome.const import (
CONF_SUPPLEMENTAL_HEATING_DELTA,
CONF_SWING_BOTH_ACTION,
CONF_SWING_HORIZONTAL_ACTION,
CONF_SWING_MODE,
CONF_SWING_OFF_ACTION,
CONF_SWING_VERTICAL_ACTION,
CONF_TARGET_TEMPERATURE_CHANGE_ACTION,
CONF_VISUAL,
)
CONF_PRESET_CHANGE = "preset_change"
CODEOWNERS = ["@kbx81"]
climate_ns = cg.esphome_ns.namespace("climate")
@ -82,6 +92,38 @@ CLIMATE_MODES = {
}
validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True)
ClimatePreset = climate_ns.enum("ClimatePreset")
PRESET_CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ThermostatClimateTargetTempConfig),
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_MODE): validate_climate_mode,
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode),
cv.Optional(CONF_SWING_MODE): cv.templatable(
climate.validate_climate_swing_mode
),
}
)
def validate_temperature_preset(preset, root_config, name, requirements):
# verify temperature settings for the provided preset / default / away configuration
for config_temp, req_actions in requirements.items():
for req_action in req_actions:
# verify corresponding default target temperature exists when a given climate action exists
if config_temp not in preset and req_action in root_config:
raise cv.Invalid(
f"{config_temp} must be defined in {name} config when using {req_action}"
)
# if a given climate action is NOT defined, it should not have a default target temperature
if config_temp in preset and req_action not in root_config:
raise cv.Invalid(
f"{config_temp} is defined in {name} config with no {req_action}"
)
def validate_thermostat(config):
# verify corresponding action(s) exist(s) for any defined climate mode or action
@ -235,33 +277,22 @@ def validate_thermostat(config):
CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION],
}
for config_temp, req_actions in requirements.items():
for req_action in req_actions:
# verify corresponding default target temperature exists when a given climate action exists
if config_temp not in config and req_action in config:
raise cv.Invalid(
f"{config_temp} must be defined when using {req_action}"
)
# if a given climate action is NOT defined, it should not have a default target temperature
if config_temp in config and req_action not in config:
raise cv.Invalid(f"{config_temp} is defined with no {req_action}")
# Validate temperature requirements for default configuraation
validate_temperature_preset(config, config, "default", requirements)
# Validate temperature requirements for away configuration
if CONF_AWAY_CONFIG in config:
away = config[CONF_AWAY_CONFIG]
for config_temp, req_actions in requirements.items():
for req_action in req_actions:
# verify corresponding default target temperature exists when a given climate action exists
if config_temp not in away and req_action in config:
raise cv.Invalid(
f"{config_temp} must be defined in away configuration when using {req_action}"
)
# if a given climate action is NOT defined, it should not have a default target temperature
if config_temp in away and req_action not in config:
raise cv.Invalid(
f"{config_temp} is defined in away configuration with no {req_action}"
)
validate_temperature_preset(away, config, "away", requirements)
# verify default climate mode is valid given above configuration
# Validate temperature requirements for presets
if CONF_PRESET in config:
for preset_config in config[CONF_PRESET]:
validate_temperature_preset(
preset_config, config, preset_config[CONF_NAME], requirements
)
# Verify default climate mode is valid given above configuration
default_mode = config[CONF_DEFAULT_MODE]
requirements = {
"HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
@ -270,13 +301,108 @@ def validate_thermostat(config):
"DRY": [CONF_DRY_ACTION],
"FAN_ONLY": [CONF_FAN_ONLY_ACTION],
"AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
}.get(default_mode, [])
for req in requirements:
"OFF": [],
}
actions_for_default_mode = requirements.get(default_mode, [])
for req in actions_for_default_mode:
if req not in config:
raise cv.Invalid(
f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration"
)
# Verify that the modes for presets are valid given the configuration
if CONF_PRESET in config:
# Preset temperature vs Visual temperature validation
# Default visual configuration from climate_traits.h
visual_min_temperature = 10.0
visual_max_temperature = 30.0
if CONF_VISUAL in config:
visual_config = config[CONF_VISUAL]
if CONF_MIN_TEMPERATURE in visual_config:
visual_min_temperature = visual_config[CONF_MIN_TEMPERATURE]
if CONF_MAX_TEMPERATURE in visual_config:
visual_max_temperature = visual_config[CONF_MAX_TEMPERATURE]
for preset_config in config[CONF_PRESET]:
if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config:
preset_min_temperature = preset_config[
CONF_DEFAULT_TARGET_TEMPERATURE_LOW
]
if preset_min_temperature < visual_min_temperature:
raise cv.Invalid(
f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} for {preset_config[CONF_NAME]} is set to {preset_min_temperature} which is less than the visual minimum temperature of {visual_min_temperature}"
)
if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config:
preset_max_temperature = preset_config[
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH
]
if preset_max_temperature > visual_max_temperature:
raise cv.Invalid(
f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} for {preset_config[CONF_NAME]} is set to {preset_max_temperature} which is more than the visual maximum temperature of {visual_max_temperature}"
)
# Mode validation
for preset_config in config[CONF_PRESET]:
if CONF_MODE not in preset_config:
continue
mode = preset_config[CONF_MODE]
for req in requirements[mode]:
if req not in config:
raise cv.Invalid(
f"{CONF_MODE} is set to {mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
)
# Fan mode requirements
requirements = {
"ON": [CONF_FAN_MODE_ON_ACTION],
"OFF": [CONF_FAN_MODE_OFF_ACTION],
"AUTO": [CONF_FAN_MODE_AUTO_ACTION],
"LOW": [CONF_FAN_MODE_LOW_ACTION],
"MEDIUM": [CONF_FAN_MODE_MEDIUM_ACTION],
"HIGH": [CONF_FAN_MODE_HIGH_ACTION],
"MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION],
"FOCUS": [CONF_FAN_MODE_FOCUS_ACTION],
"DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION],
}
for preset_config in config[CONF_PRESET]:
if CONF_FAN_MODE not in preset_config:
continue
fan_mode = preset_config[CONF_FAN_MODE]
for req in requirements[fan_mode]:
if req not in config:
raise cv.Invalid(
f"{CONF_FAN_MODE} is set to {fan_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
)
# Swing mode requirements
requirements = {
"OFF": [CONF_SWING_OFF_ACTION],
"BOTH": [CONF_SWING_BOTH_ACTION],
"VERTICAL": [CONF_SWING_VERTICAL_ACTION],
"HORIZONTAL": [CONF_SWING_HORIZONTAL_ACTION],
}
for preset_config in config[CONF_PRESET]:
if CONF_SWING_MODE not in preset_config:
continue
swing_mode = preset_config[CONF_SWING_MODE]
for req in requirements[swing_mode]:
if req not in config:
raise cv.Invalid(
f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
)
if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config:
raise cv.Invalid(
f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}"
@ -415,6 +541,10 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
}
),
cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA),
cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation(
single=True
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(
@ -531,7 +661,7 @@ async def to_code(config):
cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING]))
cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY]))
cg.add(var.set_normal_config(normal_config))
cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config))
await automation.build_automation(
var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION]
@ -694,4 +824,55 @@ async def to_code(config):
away_config = ThermostatClimateTargetTempConfig(
away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
)
cg.add(var.set_away_config(away_config))
cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config))
if CONF_PRESET in config:
for preset_config in config[CONF_PRESET]:
name = preset_config[CONF_NAME]
standard_preset = None
if name.upper() in climate.CLIMATE_PRESETS:
standard_preset = climate.CLIMATE_PRESETS[name.upper()]
if two_points_available is True:
preset_target_config = ThermostatClimateTargetTempConfig(
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
)
elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config:
preset_target_config = ThermostatClimateTargetTempConfig(
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
)
elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config:
preset_target_config = ThermostatClimateTargetTempConfig(
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
)
preset_target_variable = cg.new_variable(
preset_config[CONF_ID], preset_target_config
)
if CONF_MODE in preset_config:
cg.add(preset_target_variable.set_mode(preset_config[CONF_MODE]))
if CONF_FAN_MODE in preset_config:
cg.add(
preset_target_variable.set_fan_mode(preset_config[CONF_FAN_MODE])
)
if CONF_SWING_MODE in preset_config:
cg.add(
preset_target_variable.set_swing_mode(
preset_config[CONF_SWING_MODE]
)
)
if standard_preset is not None:
cg.add(var.set_preset_config(standard_preset, preset_target_variable))
else:
cg.add(var.set_custom_preset_config(name, preset_target_variable))
if CONF_PRESET_CHANGE in config:
await automation.build_automation(
var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE]
)

View File

@ -32,7 +32,7 @@ void ThermostatClimate::setup() {
} else {
// restore from defaults, change_away handles temps for us
this->mode = this->default_mode_;
this->change_away_(false);
this->change_preset_(climate::CLIMATE_PRESET_HOME);
}
// refresh the climate action based on the restored settings, we'll publish_state() later
this->switch_to_action_(this->compute_action_(), false);
@ -162,11 +162,20 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
if (call.get_preset().has_value()) {
// setup_complete_ blocks modifying/resetting the temps immediately after boot
if (this->setup_complete_) {
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
this->change_preset_(*call.get_preset());
} else {
this->preset = *call.get_preset();
}
}
if (call.get_custom_preset().has_value()) {
// setup_complete_ blocks modifying/resetting the temps immediately after boot
if (this->setup_complete_) {
this->change_custom_preset_(*call.get_custom_preset());
} else {
this->custom_preset = *call.get_custom_preset();
}
}
if (call.get_mode().has_value())
this->mode = *call.get_mode();
if (call.get_fan_mode().has_value())
@ -236,8 +245,12 @@ climate::ClimateTraits ThermostatClimate::traits() {
if (supports_swing_mode_vertical_)
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
if (supports_away_)
traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY});
for (auto &it : this->preset_config_) {
traits.add_supported_preset(it.first);
}
for (auto &it : this->custom_preset_config_) {
traits.add_supported_custom_preset(it.first);
}
traits.set_supports_two_point_target_temperature(this->supports_two_points_);
traits.set_supports_action(true);
@ -910,30 +923,112 @@ bool ThermostatClimate::supplemental_heating_required_() {
(this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING));
}
void ThermostatClimate::change_away_(bool away) {
if (!away) {
void ThermostatClimate::dump_preset_config_(const std::string &preset,
const ThermostatClimateTargetTempConfig &config) {
const auto *preset_name = preset.c_str();
if (this->supports_heat_) {
if (this->supports_two_points_) {
this->target_temperature_low = this->normal_config_.default_temperature_low;
this->target_temperature_high = this->normal_config_.default_temperature_high;
} else
this->target_temperature = this->normal_config_.default_temperature;
} else {
if (this->supports_two_points_) {
this->target_temperature_low = this->away_config_.default_temperature_low;
this->target_temperature_high = this->away_config_.default_temperature_high;
} else
this->target_temperature = this->away_config_.default_temperature;
ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name,
config.default_temperature_low);
} else {
ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature);
}
}
if ((this->supports_cool_) || (this->supports_fan_only_)) {
if (this->supports_two_points_) {
ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name,
config.default_temperature_high);
} else {
ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature);
}
}
if (config.mode_.has_value()) {
ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name,
LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
}
if (config.fan_mode_.has_value()) {
ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name,
LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
}
if (config.swing_mode_.has_value()) {
ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name,
LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
}
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
}
void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) {
this->normal_config_ = normal_config;
void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
auto config = this->preset_config_.find(preset);
if (config != this->preset_config_.end()) {
ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
this->change_preset_internal_(config->second);
this->custom_preset.reset();
this->preset = preset;
} else {
ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
}
}
void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) {
this->supports_away_ = true;
this->away_config_ = away_config;
void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) {
auto config = this->custom_preset_config_.find(custom_preset);
if (config != this->custom_preset_config_.end()) {
ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str());
this->change_preset_internal_(config->second);
this->preset.reset();
this->custom_preset = custom_preset;
} else {
ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str());
}
}
void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) {
if (this->supports_two_points_) {
this->target_temperature_low = config.default_temperature_low;
this->target_temperature_high = config.default_temperature_high;
} else {
this->target_temperature = config.default_temperature;
}
// Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call
// also specifies them then the control's version will override these for that call
if (config.mode_.has_value()) {
this->mode = *config.mode_;
ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
}
if (config.fan_mode_.has_value()) {
this->fan_mode = *config.fan_mode_;
ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
}
if (config.swing_mode_.has_value()) {
ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
this->swing_mode = *config.swing_mode_;
}
// Fire any preset changed trigger if defined
if (this->preset != preset) {
Trigger<> *trig = this->preset_change_trigger_;
assert(trig != nullptr);
trig->trigger();
}
this->refresh();
}
void ThermostatClimate::set_preset_config(climate::ClimatePreset preset,
const ThermostatClimateTargetTempConfig &config) {
this->preset_config_[preset] = config;
}
void ThermostatClimate::set_custom_preset_config(const std::string &name,
const ThermostatClimateTargetTempConfig &config) {
this->custom_preset_config_[name] = config;
}
ThermostatClimate::ThermostatClimate()
@ -963,7 +1058,8 @@ ThermostatClimate::ThermostatClimate()
swing_mode_off_trigger_(new Trigger<>()),
swing_mode_horizontal_trigger_(new Trigger<>()),
swing_mode_vertical_trigger_(new Trigger<>()),
temperature_change_trigger_(new Trigger<>()) {}
temperature_change_trigger_(new Trigger<>()),
preset_change_trigger_(new Trigger<>()) {}
void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; }
void ThermostatClimate::set_set_point_minimum_differential(float differential) {
@ -1112,23 +1208,11 @@ Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; }
void ThermostatClimate::dump_config() {
LOG_CLIMATE("", "Thermostat", this);
if (this->supports_heat_) {
if (this->supports_two_points_) {
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low);
} else {
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature);
}
}
if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) {
if (this->supports_two_points_) {
ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high);
} else {
ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature);
}
}
if (this->supports_two_points_)
ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_);
ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_));
@ -1194,24 +1278,21 @@ void ThermostatClimate::dump_config() {
ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_));
ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_));
ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_));
ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_));
if (this->supports_away_) {
if (this->supports_heat_) {
if (this->supports_two_points_) {
ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C",
this->away_config_.default_temperature_low);
} else {
ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature);
}
}
if ((this->supports_cool_) || (this->supports_fan_only_)) {
if (this->supports_two_points_) {
ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C",
this->away_config_.default_temperature_high);
} else {
ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature);
}
}
ESP_LOGCONFIG(TAG, " Supported PRESETS: ");
for (auto &it : this->preset_config_) {
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first));
ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true));
this->dump_preset_config_(preset_name, it.second);
}
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: ");
for (auto &it : this->custom_preset_config_) {
const auto *preset_name = it.first.c_str();
ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true));
this->dump_preset_config_(preset_name, it.second);
}
}

View File

@ -4,6 +4,7 @@
#include "esphome/core/automation.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/sensor/sensor.h"
#include <map>
namespace esphome {
namespace thermostat {
@ -34,6 +35,10 @@ struct ThermostatClimateTargetTempConfig {
ThermostatClimateTargetTempConfig(float default_temperature);
ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high);
void set_fan_mode(climate::ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; }
void set_swing_mode(climate::ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; }
void set_mode(climate::ClimateMode mode) { this->mode_ = mode; }
float default_temperature{NAN};
float default_temperature_low{NAN};
float default_temperature_high{NAN};
@ -41,6 +46,9 @@ struct ThermostatClimateTargetTempConfig {
float cool_overrun_{NAN};
float heat_deadband_{NAN};
float heat_overrun_{NAN};
optional<climate::ClimateFanMode> fan_mode_{};
optional<climate::ClimateSwingMode> swing_mode_{};
optional<climate::ClimateMode> mode_{};
};
class ThermostatClimate : public climate::Climate, public Component {
@ -94,8 +102,8 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
void set_supports_two_points(bool supports_two_points);
void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config);
void set_away_config(const ThermostatClimateTargetTempConfig &away_config);
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
Trigger<> *get_cool_action_trigger() const;
Trigger<> *get_supplemental_cool_action_trigger() const;
@ -124,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *get_swing_mode_off_trigger() const;
Trigger<> *get_swing_mode_vertical_trigger() const;
Trigger<> *get_temperature_change_trigger() const;
Trigger<> *get_preset_change_trigger() const;
/// Get current hysteresis values
float cool_deadband();
float cool_overrun();
@ -149,8 +158,14 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
/// Change the away setting, will reset target temperatures to defaults.
void change_away_(bool away);
/// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly
void change_preset_(climate::ClimatePreset preset);
/// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly
void change_custom_preset_(const std::string &custom_preset);
/// Applies the temperature, mode, fan, and swing modes of the provded config.
/// This is agnostic of custom vs built in preset
void change_preset_internal_(const ThermostatClimateTargetTempConfig &config);
/// Return the traits of this controller.
climate::ClimateTraits traits() override;
@ -210,6 +225,8 @@ class ThermostatClimate : public climate::Climate, public Component {
bool supplemental_cooling_required_();
bool supplemental_heating_required_();
void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config);
/// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr};
@ -267,11 +284,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// A false value means that the controller has no such support.
bool supports_two_points_{false};
/// Whether the controller supports an "away" mode
///
/// A false value means that the controller has no such mode.
bool supports_away_{false};
/// Flags indicating if maximum allowable run time was exceeded
bool cooling_max_runtime_exceeded_{false};
bool heating_max_runtime_exceeded_{false};
@ -368,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The trigger to call when the target temperature(s) change(es).
Trigger<> *temperature_change_trigger_{nullptr};
/// The triggr to call when the preset mode changes
Trigger<> *preset_change_trigger_{nullptr};
/// A reference to the trigger that was previously active.
///
/// This is so that the previous trigger can be stopped before enabling a new one
@ -409,10 +424,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Minimum allowable duration in seconds for action timers
const uint8_t min_timer_duration_{1};
/// Temperature data for normal/home and away modes
ThermostatClimateTargetTempConfig normal_config_{};
ThermostatClimateTargetTempConfig away_config_{};
/// Climate action timers
std::vector<ThermostatClimateTimer> timer_{
{"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)},
@ -425,6 +436,11 @@ class ThermostatClimate : public climate::Climate, public Component {
{"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)},
{"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)},
{"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}};
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
};
} // namespace thermostat

View File

@ -1,5 +1,6 @@
from esphome.components import time
from esphome import automation
from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
@ -11,6 +12,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints"
CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update"
CONF_DATAPOINT_TYPE = "datapoint_type"
CONF_STATUS_PIN = "status_pin"
tuya_ns = cg.esphome_ns.namespace("tuya")
Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice)
@ -88,6 +90,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(
cv.uint8_t
),
cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -114,6 +117,9 @@ async def to_code(config):
if CONF_TIME_ID in config:
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time_id(time_))
if CONF_STATUS_PIN in config:
status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN])
cg.add(var.set_status_pin(status_pin_))
if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config:
for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]:
cg.add(var.add_ignore_mcu_update_on_datapoints(dp))

View File

@ -1,5 +1,4 @@
#include "esphome/core/log.h"
#include "esphome/components/fan/fan_helpers.h"
#include "tuya_fan.h"
namespace esphome {

View File

@ -10,6 +10,11 @@ static const char *const TAG = "tuya.light";
void TuyaLight::setup() {
if (this->color_temperature_id_.has_value()) {
this->parent_->register_listener(*this->color_temperature_id_, [this](const TuyaDatapoint &datapoint) {
if (this->state_->current_values != this->state_->remote_values) {
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
return;
}
auto datapoint_value = datapoint.value_uint;
if (this->color_temperature_invert_) {
datapoint_value = this->color_temperature_max_value_ - datapoint_value;
@ -23,6 +28,11 @@ void TuyaLight::setup() {
}
if (this->dimmer_id_.has_value()) {
this->parent_->register_listener(*this->dimmer_id_, [this](const TuyaDatapoint &datapoint) {
if (this->state_->current_values != this->state_->remote_values) {
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
return;
}
auto call = this->state_->make_call();
call.set_brightness(float(datapoint.value_uint) / this->max_value_);
call.perform();
@ -30,6 +40,11 @@ void TuyaLight::setup() {
}
if (switch_id_.has_value()) {
this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) {
if (this->state_->current_values != this->state_->remote_values) {
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
return;
}
auto call = this->state_->make_call();
call.set_state(datapoint.value_bool);
call.perform();
@ -41,6 +56,11 @@ void TuyaLight::setup() {
auto green = parse_hex<uint8_t>(datapoint.value_string.substr(2, 2));
auto blue = parse_hex<uint8_t>(datapoint.value_string.substr(4, 2));
if (red.has_value() && green.has_value() && blue.has_value()) {
if (this->state_->current_values != this->state_->remote_values) {
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
return;
}
auto call = this->state_->make_call();
call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255);
call.perform();
@ -52,6 +72,11 @@ void TuyaLight::setup() {
auto saturation = parse_hex<uint16_t>(datapoint.value_string.substr(4, 4));
auto value = parse_hex<uint16_t>(datapoint.value_string.substr(8, 4));
if (hue.has_value() && saturation.has_value() && value.has_value()) {
if (this->state_->current_values != this->state_->remote_values) {
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
return;
}
float red, green, blue;
hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue);
auto call = this->state_->make_call();

View File

@ -0,0 +1,47 @@
from esphome.components import select
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_OPTIONS, CONF_OPTIMISTIC, CONF_ENUM_DATAPOINT
from .. import tuya_ns, CONF_TUYA_ID, Tuya
DEPENDENCIES = ["tuya"]
CODEOWNERS = ["@bearpawmaxim"]
TuyaSelect = tuya_ns.class_("TuyaSelect", select.Select, cg.Component)
def ensure_option_map(value):
cv.check_not_templatable(value)
option = cv.All(cv.int_range(0, 2**8 - 1))
mapping = cv.All(cv.string_strict)
options_map_schema = cv.Schema({option: mapping})
value = options_map_schema(value)
all_values = list(value.keys())
unique_values = set(value.keys())
if len(all_values) != len(unique_values):
raise cv.Invalid("Mapping values must be unique.")
return value
CONFIG_SCHEMA = select.SELECT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TuyaSelect),
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t,
cv.Required(CONF_OPTIONS): ensure_option_map,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
options_map = config[CONF_OPTIONS]
var = await select.new_select(config, options=list(options_map.values()))
await cg.register_component(var, config)
cg.add(var.set_select_mappings(list(options_map.keys())))
parent = await cg.get_variable(config[CONF_TUYA_ID])
cg.add(var.set_tuya_parent(parent))
cg.add(var.set_select_id(config[CONF_ENUM_DATAPOINT]))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))

View File

@ -0,0 +1,52 @@
#include "esphome/core/log.h"
#include "tuya_select.h"
namespace esphome {
namespace tuya {
static const char *const TAG = "tuya.select";
void TuyaSelect::setup() {
this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) {
uint8_t enum_value = datapoint.value_enum;
ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value);
auto options = this->traits.get_options();
auto mappings = this->mappings_;
auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value);
if (it == mappings.end()) {
ESP_LOGW(TAG, "Invalid value %u", enum_value);
return;
}
size_t mapping_idx = std::distance(mappings.cbegin(), it);
auto value = this->at(mapping_idx);
this->publish_state(value.value());
});
}
void TuyaSelect::control(const std::string &value) {
if (this->optimistic_)
this->publish_state(value);
auto idx = this->index_of(value);
if (idx.has_value()) {
uint8_t mapping = this->mappings_.at(idx.value());
ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str());
this->parent_->set_enum_datapoint_value(this->select_id_, mapping);
return;
}
ESP_LOGW(TAG, "Invalid value %s", value.c_str());
}
void TuyaSelect::dump_config() {
LOG_SELECT("", "Tuya Select", this);
ESP_LOGCONFIG(TAG, " Select has datapoint ID %u", this->select_id_);
ESP_LOGCONFIG(TAG, " Options are:");
auto options = this->traits.get_options();
for (auto i = 0; i < this->mappings_.size(); i++) {
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str());
}
}
} // namespace tuya
} // namespace esphome

View File

@ -0,0 +1,30 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/tuya/tuya.h"
#include "esphome/components/select/select.h"
namespace esphome {
namespace tuya {
class TuyaSelect : public select::Select, public Component {
public:
void setup() override;
void dump_config() override;
void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_select_id(uint8_t select_id) { this->select_id_ = select_id; }
void set_select_mappings(std::vector<uint8_t> mappings) { this->mappings_ = std::move(mappings); }
protected:
void control(const std::string &value) override;
Tuya *parent_;
bool optimistic_ = false;
uint8_t select_id_;
std::vector<uint8_t> mappings_;
};
} // namespace tuya
} // namespace esphome

View File

@ -20,7 +20,7 @@ void TuyaTextSensor::setup() {
break;
}
default:
ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, datapoint.type);
ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type);
break;
}
});

View File

@ -3,6 +3,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/gpio.h"
namespace esphome {
namespace tuya {
@ -14,6 +15,9 @@ static const int MAX_RETRIES = 5;
void Tuya::setup() {
this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
if (this->status_pin_.has_value()) {
this->status_pin_.value()->digital_write(false);
}
}
void Tuya::loop() {
@ -54,9 +58,12 @@ void Tuya::dump_config() {
ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id);
}
}
if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_,
this->gpio_reset_);
if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) {
ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_,
this->reset_pin_reported_);
}
if (this->status_pin_.has_value()) {
LOG_PIN(" Status Pin: ", this->status_pin_.value());
}
ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str());
this->check_uart_settings(9600);
@ -171,16 +178,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
}
case TuyaCommandType::CONF_QUERY: {
if (len >= 2) {
this->gpio_status_ = buffer[0];
this->gpio_reset_ = buffer[1];
this->status_pin_reported_ = buffer[0];
this->reset_pin_reported_ = buffer[1];
}
if (this->init_state_ == TuyaInitState::INIT_CONF) {
// If mcu returned status gpio, then we can omit sending wifi state
if (this->gpio_status_ != -1) {
if (this->status_pin_reported_ != -1) {
this->init_state_ = TuyaInitState::INIT_DATAPOINT;
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
bool is_pin_equals =
this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_;
// Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send
if (is_pin_equals) {
ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_);
this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); });
} else {
ESP_LOGW(TAG, "Supplied status_pin does not equals the reported pin %i. TuyaMcu will work in limited mode.",
this->status_pin_reported_);
}
} else {
this->init_state_ = TuyaInitState::INIT_WIFI;
ESP_LOGV(TAG, "Configured WIFI_STATE periodic send");
this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); });
}
}
@ -415,16 +433,19 @@ void Tuya::send_empty_command_(TuyaCommandType command) {
send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{}});
}
void Tuya::set_status_pin_() {
bool is_network_ready = network::is_connected() && remote_is_connected();
this->status_pin_.value()->digital_write(is_network_ready);
}
void Tuya::send_wifi_status_() {
uint8_t status = 0x02;
if (network::is_connected()) {
status = 0x03;
// Protocol version 3 also supports specifying when connected to "the cloud"
if (this->protocol_version_ >= 0x03) {
if (remote_is_connected()) {
status = 0x04;
}
if (this->protocol_version_ >= 0x03 && remote_is_connected()) {
status = 0x04;
}
}

View File

@ -79,6 +79,7 @@ class Tuya : public Component, public uart::UARTDevice {
void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value);
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value);
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value);
void set_status_pin(InternalGPIOPin *status_pin) { this->status_pin_ = status_pin; }
void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value);
void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value);
void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length);
@ -115,6 +116,7 @@ class Tuya : public Component, public uart::UARTDevice {
void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced);
void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector<uint8_t> &value, bool forced);
void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data);
void set_status_pin_();
void send_wifi_status_();
#ifdef USE_TIME
@ -125,8 +127,9 @@ class Tuya : public Component, public uart::UARTDevice {
bool init_failed_{false};
int init_retries_{0};
uint8_t protocol_version_ = -1;
int gpio_status_ = -1;
int gpio_reset_ = -1;
optional<InternalGPIOPin *> status_pin_{};
int status_pin_reported_ = -1;
int reset_pin_reported_ = -1;
uint32_t last_command_timestamp_ = 0;
uint32_t last_rx_char_timestamp_ = 0;
std::string product_ = "";

View File

@ -36,6 +36,8 @@ class WaveshareEPaper : public PollingComponent,
void on_safe_shutdown() override;
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;

View File

@ -21,10 +21,6 @@
#include "esphome/components/logger/logger.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan_helpers.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate.h"
#endif
@ -482,22 +478,6 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
root["speed_count"] = traits.supported_speed_count();
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) {
case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations)
root["speed"] = "low";
break;
case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations)
root["speed"] = "medium";
break;
case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations)
root["speed"] = "high";
break;
}
#pragma GCC diagnostic pop
}
if (obj->get_traits().supports_oscillation())
root["oscillation"] = obj->oscillating;
@ -518,10 +498,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
auto call = obj->turn_on();
if (request->hasParam("speed")) {
String speed = request->getParam("speed")->value();
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations)
#pragma GCC diagnostic pop
}
if (request->hasParam("speed_level")) {
String speed_level = request->getParam("speed_level")->value();

View File

@ -555,6 +555,7 @@ void WiFiComponent::check_connecting_finished() {
}
ESP_LOGW(TAG, "WiFi Unknown connection status %d", (int) status);
this->retry_connect();
}
void WiFiComponent::retry_connect() {

View File

@ -1086,16 +1086,21 @@ def possibly_negative_percentage(value):
except ValueError:
# pylint: disable=raise-missing-from
raise Invalid("invalid number")
if value > 1:
msg = "Percentage must not be higher than 100%."
if not has_percent_sign:
msg += " Please put a percent sign after the number!"
raise Invalid(msg)
if value < -1:
msg = "Percentage must not be smaller than -100%."
if not has_percent_sign:
msg += " Please put a percent sign after the number!"
raise Invalid(msg)
try:
if value > 1:
msg = "Percentage must not be higher than 100%."
if not has_percent_sign:
msg += " Please put a percent sign after the number!"
raise Invalid(msg)
if value < -1:
msg = "Percentage must not be smaller than -100%."
if not has_percent_sign:
msg += " Please put a percent sign after the number!"
raise Invalid(msg)
except TypeError:
raise Invalid( # pylint: disable=raise-missing-from
"Expected percentage or float between -1.0 and 1.0"
)
return negative_one_to_one_float(value)

View File

@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2022.5.1"
__version__ = "2022.6.0"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
@ -105,6 +105,7 @@ CONF_COLOR_BRIGHTNESS = "color_brightness"
CONF_COLOR_CORRECT = "color_correct"
CONF_COLOR_INTERLOCK = "color_interlock"
CONF_COLOR_MODE = "color_mode"
CONF_COLOR_PALETTE = "color_palette"
CONF_COLOR_TEMPERATURE = "color_temperature"
CONF_COLORS = "colors"
CONF_COMMAND = "command"
@ -198,6 +199,7 @@ CONF_ENABLE_TIME = "enable_time"
CONF_ENERGY = "energy"
CONF_ENTITY_CATEGORY = "entity_category"
CONF_ENTITY_ID = "entity_id"
CONF_ENUM_DATAPOINT = "enum_datapoint"
CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
CONF_ESPHOME = "esphome"
CONF_ETHERNET = "ethernet"

View File

@ -45,6 +45,9 @@
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
#ifdef USE_MEDIA_PLAYER
#include "esphome/components/media_player/media_player.h"
#endif
namespace esphome {
@ -111,6 +114,10 @@ class Application {
void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); }
#endif
#ifdef USE_MEDIA_PLAYER
void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); }
#endif
/// Register the component in this Application instance.
template<class C> C *register_component(C *c) {
static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
@ -273,6 +280,15 @@ class Application {
return nullptr;
}
#endif
#ifdef USE_MEDIA_PLAYER
const std::vector<media_player::MediaPlayer *> &get_media_players() { return this->media_players_; }
media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->media_players_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
Scheduler scheduler;
@ -324,6 +340,9 @@ class Application {
#ifdef USE_LOCK
std::vector<lock::Lock *> locks_{};
#endif
#ifdef USE_MEDIA_PLAYER
std::vector<media_player::MediaPlayer *> media_players_{};
#endif
std::string name_;
std::string compilation_time_;

View File

@ -231,6 +231,21 @@ void ComponentIterator::advance() {
}
}
break;
#endif
#ifdef USE_MEDIA_PLAYER
case IteratorState::MEDIA_PLAYER:
if (this->at_ >= App.get_media_players().size()) {
advance_platform = true;
} else {
auto *media_player = App.get_media_players()[this->at_];
if (media_player->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_media_player(media_player);
}
}
break;
#endif
case IteratorState::MAX:
if (this->on_end()) {
@ -254,4 +269,7 @@ bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return
#ifdef USE_ESP32_CAMERA
bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; }
#endif
#ifdef USE_MEDIA_PLAYER
bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; }
#endif
} // namespace esphome

View File

@ -62,6 +62,9 @@ class ComponentIterator {
#endif
#ifdef USE_LOCK
virtual bool on_lock(lock::Lock *a_lock) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual bool on_media_player(media_player::MediaPlayer *media_player);
#endif
virtual bool on_end();
@ -110,6 +113,9 @@ class ComponentIterator {
#endif
#ifdef USE_LOCK
LOCK,
#endif
#ifdef USE_MEDIA_PLAYER
MEDIA_PLAYER,
#endif
MAX,
} state_{IteratorState::NONE};

View File

@ -73,6 +73,12 @@ void Controller::setup_controller(bool include_internal) {
obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); });
}
#endif
#ifdef USE_MEDIA_PLAYER
for (auto *obj : App.get_media_players()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); });
}
#endif
}
} // namespace esphome

View File

@ -37,6 +37,9 @@
#ifdef USE_LOCK
#include "esphome/components/lock/lock.h"
#endif
#ifdef USE_MEDIA_PLAYER
#include "esphome/components/media_player/media_player.h"
#endif
namespace esphome {
@ -76,6 +79,9 @@ class Controller {
#ifdef USE_LOCK
virtual void on_lock_update(lock::Lock *obj){};
#endif
#ifdef USE_MEDIA_PLAYER
virtual void on_media_player_update(media_player::MediaPlayer *obj){};
#endif
};
} // namespace esphome

View File

@ -29,6 +29,7 @@
#define USE_LOCK
#define USE_LOGGER
#define USE_MDNS
#define USE_MEDIA_PLAYER
#define USE_MQTT
#define USE_NUMBER
#define USE_OTA_PASSWORD

View File

@ -46,7 +46,9 @@ class EntityBase {
void set_icon(const std::string &name);
protected:
virtual uint32_t hash_base() = 0;
/// The hash_base() function has been deprecated. It is kept in this
/// class for now, to prevent external components from not compiling.
virtual uint32_t hash_base() { return 0L; }
void calc_object_id_();
std::string name_;

View File

@ -251,7 +251,49 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors
@_add_data_ref
def construct_include(self, node):
return _load_yaml_internal(self._rel_path(node.value))
def extract_file_vars(node):
fields = self.construct_yaml_map(node)
file = fields.get("file")
if file is None:
raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark)
vars = fields.get("vars")
if vars:
vars = {k: str(v) for k, v in vars.items()}
return file, vars
def substitute_vars(config, vars):
from esphome.const import CONF_SUBSTITUTIONS
from esphome.components import substitutions
org_subs = None
result = config
if not isinstance(config, dict):
# when the included yaml contains a list or a scalar
# wrap it into an OrderedDict because do_substitution_pass expects it
result = OrderedDict([("yaml", config)])
elif CONF_SUBSTITUTIONS in result:
org_subs = result.pop(CONF_SUBSTITUTIONS)
result[CONF_SUBSTITUTIONS] = vars
# Ignore missing vars that refer to the top level substitutions
substitutions.do_substitution_pass(result, None, ignore_missing=True)
result.pop(CONF_SUBSTITUTIONS)
if not isinstance(config, dict):
result = result["yaml"] # unwrap the result
elif org_subs:
result[CONF_SUBSTITUTIONS] = org_subs
return result
if isinstance(node, yaml.nodes.MappingNode):
file, vars = extract_file_vars(node)
else:
file, vars = node.value, None
result = _load_yaml_internal(self._rel_path(file))
if vars:
result = substitute_vars(result, vars)
return result
@_add_data_ref
def construct_include_dir_list(self, node):

View File

@ -39,6 +39,8 @@ lib_deps =
bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library
build_flags =
-DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
src_filter =
@ -118,10 +120,12 @@ lib_deps =
HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in)
esphome/ESP32-audioI2S@2.1.0 ; i2s_audio
build_flags =
${common:arduino.build_flags}
-DUSE_ESP32
-DUSE_ESP32_FRAMEWORK_ARDUINO
-DAUDIO_NO_SD_FS ; i2s_audio
extra_scripts = post:esphome/components/esp32/post_build.py.script
; This are common settings for the ESP32 (all variants) using IDF.

View File

@ -7,7 +7,7 @@ tzlocal==4.2 # from time
tzdata>=2021.1 # from time
pyserial==3.5
platformio==5.2.5 # When updating platformio, also update Dockerfile
esptool==3.3
esptool==3.3.1
click==8.1.3
esphome-dashboard==20220508.0
aioesphomeapi==10.8.2

View File

@ -1,4 +1,4 @@
pylint==2.13.8
pylint==2.13.9
flake8==4.0.1
black==22.3.0
pyupgrade==2.32.1

View File

@ -168,6 +168,13 @@ mqtt:
id: uart0
data: !lambda |-
return {};
on_connect:
- light.turn_on: ${roomname}_lights
- mqtt.publish:
topic: some/topic
payload: Hello
on_disconnect:
- light.turn_off: ${roomname}_lights
i2c:
sda: 21
@ -1886,6 +1893,7 @@ climate:
- platform: bedjet
name: My Bedjet
ble_client_id: my_bedjet_ble_client
heat_mode: extended
script:
- id: climate_custom
@ -1901,14 +1909,6 @@ script:
preset: SLEEP
switch:
- platform: template
name: MIDEA_AC_TOGGLE_LIGHT
turn_on_action:
midea_ac.display_toggle:
- platform: template
name: MIDEA_AC_SWING_STEP
turn_on_action:
midea_ac.swing_step:
- platform: template
name: MIDEA_AC_BEEPER_CONTROL
optimistic: true
@ -2834,3 +2834,23 @@ button:
id: scd40
- scd4x.factory_reset:
id: scd40
- platform: template
name: Midea Display Toggle
on_press:
midea_ac.display_toggle:
- platform: template
name: Midea Swing Step
on_press:
midea_ac.swing_step:
- platform: template
name: Midea Power On
on_press:
midea_ac.power_on:
- platform: template
name: Midea Power Off
on_press:
midea_ac.power_off:
- platform: template
name: Midea Power Inverse
on_press:
midea_ac.power_toggle:

View File

@ -281,10 +281,27 @@ sensor:
window_correction_factor: 1.0
address: 0x53
update_interval: 60s
- platform: sgp40
name: 'Workshop VOC'
- platform: sgp4x
voc:
name: "VOC Index"
id: sgp40_voc_index
algorithm_tuning:
index_offset: 100
learning_time_offset_hours: 12
learning_time_gain_hours: 12
gating_max_duration_minutes: 180
std_initial: 50
gain_factor: 230
nox:
name: "NOx"
algorithm_tuning:
index_offset: 100
learning_time_offset_hours: 12
learning_time_gain_hours: 12
gating_max_duration_minutes: 180
std_initial: 50
gain_factor: 230
update_interval: 5s
store_baseline: 'true'
- platform: mcp3008
update_interval: 5s
mcp3008_id: 'mcp3008_hub'

View File

@ -57,6 +57,18 @@ time:
tuya:
time_id: sntp_time
status_pin:
number: 14
inverted: true
select:
- platform: tuya
id: tuya_select
enum_datapoint: 42
options:
0: Internal
1: Floor
2: Both
pipsolar:
id: inverter0
@ -592,3 +604,20 @@ touchscreen:
- logger.log:
format: Touch at (%d, %d)
args: ["touch.x", "touch.y"]
- media_player.play:
- media_player.pause:
- media_player.stop:
- media_player.toggle:
- media_player.volume_up:
- media_player.volume_down:
- media_player.volume_set: 50%
media_player:
- platform: i2s_audio
name: ${friendly_name}
dac_type: external
i2s_lrclk_pin: GPIO26
i2s_dout_pin: GPIO25
i2s_bclk_pin: GPIO27
mute_pin: GPIO14

View File

@ -0,0 +1,2 @@
---
ssid: ${name}

View File

@ -0,0 +1,2 @@
---
- ${var1}

View File

@ -0,0 +1 @@
${var1}

View File

@ -0,0 +1,17 @@
---
substitutions:
name: original
wifi: !include
file: includes/included.yaml
vars:
name: my_custom_ssid
esphome:
# should be substituted as 'original', not overwritten by vars in the !include above
name: ${name}
name_add_mac_suffix: true
platform: esp8266
board: !include { file: includes/scalar.yaml, vars: { var1: nodemcu } }
libraries: !include { file: includes/list.yaml, vars: { var1: Wire } }

View File

@ -0,0 +1,13 @@
from esphome import yaml_util
from esphome.components import substitutions
def test_include_with_vars(fixture_path):
yaml_file = fixture_path / "yaml_util" / "includetest.yaml"
actual = yaml_util.load_yaml(yaml_file)
substitutions.do_substitution_pass(actual, None)
assert actual["esphome"]["name"] == "original"
assert actual["esphome"]["libraries"][0] == "Wire"
assert actual["esphome"]["board"] == "nodemcu"
assert actual["wifi"]["ssid"] == "my_custom_ssid"