Merge branch 'dev' into bump-2022.6.0b1

This commit is contained in:
Jesse Hills 2022-06-08 22:46:20 +12:00
commit 9663760ec5
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
155 changed files with 3503 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

@ -4,6 +4,7 @@
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/climate/climate.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "bedjet_base.h"
@ -37,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();
@ -73,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);
@ -84,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,
@ -66,8 +74,8 @@ enum BedjetCommand : uint8_t {
#define BEDJET_FAN_STEP_NAMES_ \
{ \
" 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \
" 75%", " 80%", " 85%", " 90%", " 95%", "100%" \
"5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \
"85%", "90%", "95%", "100%" \
}
static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_;

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

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

@ -156,7 +156,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
return true;
}
return to_save.data == stored_data.data;
return to_save.data != stored_data.data;
}
};

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,140 @@
#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;
}
}
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

@ -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,70 @@
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)
)
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.Required(CONF_ID): 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)
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
@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,35 @@
#pragma once
#include "esphome/core/automation.h"
#include "media_player.h"
namespace esphome {
namespace media_player {
template<typename... Ts> class PlayAction : public Action<Ts...>, public Parented<MediaPlayer> {
void play(Ts... x) override {
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY).perform();
}
};
template<typename... Ts> class ToggleAction : public Action<Ts...>, public Parented<MediaPlayer> {
void play(Ts... x) override {
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_TOGGLE).perform();
}
};
template<typename... Ts> class PauseAction : public Action<Ts...>, public Parented<MediaPlayer> {
void play(Ts... x) override {
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PAUSE).perform();
}
};
template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<MediaPlayer> {
void play(Ts... x) override {
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP).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,91 @@
#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
};
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,8 @@ 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();
}
}
bool MQTTFanComponent::publish_state() {
@ -148,31 +132,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

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

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