mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 15:41:52 +00:00
Compare commits
310 Commits
jesserockz
...
2022.6.0b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c7c0834c0 | ||
|
|
f3a25de11d | ||
|
|
041bef8bcd | ||
|
|
6e83790308 | ||
|
|
d2d4eb4eae | ||
|
|
5942a3898c | ||
|
|
93421f0fa7 | ||
|
|
6cb5cd48c2 | ||
|
|
746fd1122f | ||
|
|
9663760ec5 | ||
|
|
a3d73d1e23 | ||
|
|
d63e14a4b6 | ||
|
|
03944e6cd8 | ||
|
|
0d1028be2e | ||
|
|
6a85259e4d | ||
|
|
ebca936b7e | ||
|
|
31c4551890 | ||
|
|
dd470d4197 | ||
|
|
612822490b | ||
|
|
f8969605e8 | ||
|
|
dd24ffa24e | ||
|
|
d0dda48932 | ||
|
|
6349b5f654 | ||
|
|
a6ff02a3cf | ||
|
|
4f57bf786b | ||
|
|
6221f6d47d | ||
|
|
a922efeafa | ||
|
|
5aa42e5e66 | ||
|
|
708672ec7e | ||
|
|
d2cefbf224 | ||
|
|
adb7aa6950 | ||
|
|
c5069edc78 | ||
|
|
282d9e138c | ||
|
|
72fcf2cbe1 | ||
|
|
6f49f5465b | ||
|
|
17b8bd8316 | ||
|
|
7e88938932 | ||
|
|
c707e64685 | ||
|
|
a639690716 | ||
|
|
01222dbab7 | ||
|
|
ff72d6a146 | ||
|
|
603d0d0c7c | ||
|
|
28883f711b | ||
|
|
e914828add | ||
|
|
c1480029fb | ||
|
|
40f622949e | ||
|
|
63096ac2bc | ||
|
|
c2a59cb476 | ||
|
|
d6e039a1d1 | ||
|
|
0f1a7c2b69 | ||
|
|
41d9059a2f | ||
|
|
e26e0d7c01 | ||
|
|
ad41c07a1f | ||
|
|
5732f3b044 | ||
|
|
712115b6ce | ||
|
|
9283559c6b | ||
|
|
6b393438e9 | ||
|
|
343b9ab455 | ||
|
|
dcb226b202 | ||
|
|
2243021b58 | ||
|
|
d5134e88b1 | ||
|
|
c59adf612f | ||
|
|
a82d8ea0c3 | ||
|
|
ad57faa9a9 | ||
|
|
a9b5e8d036 | ||
|
|
59e6e798dd | ||
|
|
e5c2dbc7ec | ||
|
|
756f71c382 | ||
|
|
b7535693fa | ||
|
|
06a3505698 | ||
|
|
efa8f0730d | ||
|
|
023d26f521 | ||
|
|
5068619f1b | ||
|
|
b528f48417 | ||
|
|
ec7a79049a | ||
|
|
6ddad6b299 | ||
|
|
16dc7762f9 | ||
|
|
dc0ed8857f | ||
|
|
bb6b77bd98 | ||
|
|
dcc80f9032 | ||
|
|
dd554bcdf4 | ||
|
|
f376a39e55 | ||
|
|
8dcc9d6b66 | ||
|
|
a576c9f21f | ||
|
|
71a438e2cb | ||
|
|
272d6f2a8b | ||
|
|
5dc776e55f | ||
|
|
72d60f30f7 | ||
|
|
869743a742 | ||
|
|
7b03e07908 | ||
|
|
348f880e15 | ||
|
|
ead597d0fb | ||
|
|
afbf989715 | ||
|
|
01b62a16c3 | ||
|
|
c5eba04517 | ||
|
|
282313ab52 | ||
|
|
d274545e77 | ||
|
|
d3fda37615 | ||
|
|
cbe3092404 | ||
|
|
6dfe3039d0 | ||
|
|
d6009453df | ||
|
|
c81323ef91 | ||
|
|
961c27f1c2 | ||
|
|
fe4a14e6cc | ||
|
|
50848c2f4d | ||
|
|
d32633b3c7 | ||
|
|
b37739eec2 | ||
|
|
28f87dc804 | ||
|
|
41879e41e6 | ||
|
|
fc0a6546a2 | ||
|
|
ffd4280d6c | ||
|
|
db3b955b0f | ||
|
|
5516f65971 | ||
|
|
9471df0a1b | ||
|
|
6d39f64be7 | ||
|
|
b89d0a9a73 | ||
|
|
4bb779d9a5 | ||
|
|
386a5b6362 | ||
|
|
e32a999cd0 | ||
|
|
bfbc6a4bad | ||
|
|
8c9e0e552d | ||
|
|
8aaf9fd83f | ||
|
|
08057720b8 | ||
|
|
bfaa648837 | ||
|
|
d504daef91 | ||
|
|
b8d3ef2f49 | ||
|
|
3bf6320030 | ||
|
|
708b928c73 | ||
|
|
649366ff44 | ||
|
|
e5c9e87fad | ||
|
|
f3d9d707b6 | ||
|
|
090e10730c | ||
|
|
fbc84861c7 | ||
|
|
e763469af8 | ||
|
|
3c0c514e44 | ||
|
|
ed5e2dd332 | ||
|
|
09b7c6f550 | ||
|
|
df315a1f51 | ||
|
|
7ee4bb621c | ||
|
|
24874f4c3c | ||
|
|
c128880033 | ||
|
|
a66e94a0b0 | ||
|
|
56870ed4a8 | ||
|
|
3ac720df47 | ||
|
|
1bc757ad06 | ||
|
|
f72abc6f3d | ||
|
|
5ac88de985 | ||
|
|
0826b367d6 | ||
|
|
329bf861d6 | ||
|
|
9dcd3d18a0 | ||
|
|
db66cd88b6 | ||
|
|
86c205fe43 | ||
|
|
c6414138c7 | ||
|
|
36b355eb82 | ||
|
|
7be9291b13 | ||
|
|
ea9e75039b | ||
|
|
a5fb036011 | ||
|
|
e55506f9db | ||
|
|
50ec1d0445 | ||
|
|
3d5e1d8d91 | ||
|
|
db2128a344 | ||
|
|
21db43db06 | ||
|
|
5009b3029f | ||
|
|
57a029189c | ||
|
|
0cb715bb76 | ||
|
|
7d03823afd | ||
|
|
8e1c9f5042 | ||
|
|
980b7cda8f | ||
|
|
3a72dd5cb6 | ||
|
|
3178243811 | ||
|
|
d30e2f2a4f | ||
|
|
6226dae05c | ||
|
|
9c6a475a6e | ||
|
|
8294d10d5b | ||
|
|
67558bec47 | ||
|
|
84873d4074 | ||
|
|
58a0b28a39 | ||
|
|
b37d3a66cc | ||
|
|
7e495a5e27 | ||
|
|
c41547fd4a | ||
|
|
0d47d41c85 | ||
|
|
41a3a17456 | ||
|
|
cbbafbcca2 | ||
|
|
c75566b374 | ||
|
|
7279f1fcc1 | ||
|
|
d7432f7c10 | ||
|
|
b0a0a153f3 | ||
|
|
024632dbd0 | ||
|
|
0a545a28b9 | ||
|
|
0f2df59998 | ||
|
|
29a7d32f77 | ||
|
|
687a7e9b2f | ||
|
|
09e8782318 | ||
|
|
f2aea02210 | ||
|
|
194f922312 | ||
|
|
fea3c48098 | ||
|
|
c2f57baec2 | ||
|
|
f4a140e126 | ||
|
|
ab506b09fe | ||
|
|
87e1cdeedb | ||
|
|
81a36146ef | ||
|
|
7fa4a68a27 | ||
|
|
f1c5e2ef81 | ||
|
|
b526155cce | ||
|
|
62c3f301e7 | ||
|
|
38cb988809 | ||
|
|
b976ac54c8 | ||
|
|
78026e766f | ||
|
|
b4cd8d21a5 | ||
|
|
7552893311 | ||
|
|
21c896d8f8 | ||
|
|
4b7fe202ec | ||
|
|
9f4519210f | ||
|
|
b0506afa5b | ||
|
|
8cbb379898 | ||
|
|
b226215593 | ||
|
|
19970729a9 | ||
|
|
d2ebfd2833 | ||
|
|
bd782fc828 | ||
|
|
23560e608c | ||
|
|
f1377b560e | ||
|
|
72108684ea | ||
|
|
c6adaaea97 | ||
|
|
91999a38ca | ||
|
|
b34eed125d | ||
|
|
2abe09529a | ||
|
|
9aaaf4dd4b | ||
|
|
cbfbcf7f1b | ||
|
|
c7651dc40d | ||
|
|
eda1c471ad | ||
|
|
c7ef18fbc4 | ||
|
|
901ec918b1 | ||
|
|
6bdae55ee1 | ||
|
|
dfb96e4b7f | ||
|
|
ff2c316b18 | ||
|
|
5be52f71f9 | ||
|
|
42873dd37c | ||
|
|
f93e7d4e3a | ||
|
|
bbcd523967 | ||
|
|
68cbe58d00 | ||
|
|
115bca98f1 | ||
|
|
ed0b34b2fe | ||
|
|
ab34401421 | ||
|
|
eed0c18d65 | ||
|
|
e5a38ce748 | ||
|
|
7d9d9fcf36 | ||
|
|
f0aba6ceb2 | ||
|
|
ab07ee57c6 | ||
|
|
eae3d72a4d | ||
|
|
7b8d826704 | ||
|
|
e7baa42e63 | ||
|
|
2f32833a22 | ||
|
|
f6935a4b4b | ||
|
|
332c9e891b | ||
|
|
b91ee4847f | ||
|
|
625463d871 | ||
|
|
6433a01e07 | ||
|
|
56cc31e8e7 | ||
|
|
3af297aa76 | ||
|
|
996ec59d28 | ||
|
|
95593eeeab | ||
|
|
dad244fb7a | ||
|
|
adb5d27d95 | ||
|
|
8456a8cecb | ||
|
|
b9f66373c1 | ||
|
|
9ac365feef | ||
|
|
43bbd58a44 | ||
|
|
7feffa64f3 | ||
|
|
ea0977abb4 | ||
|
|
4c83dc7c28 | ||
|
|
e10ab1da78 | ||
|
|
1b0e60374b | ||
|
|
3a760fbb44 | ||
|
|
6ef57a2973 | ||
|
|
3e9c7f2e9f | ||
|
|
430598b7a1 | ||
|
|
91611b09b4 | ||
|
|
ecd115851f | ||
|
|
4a1e50fed1 | ||
|
|
d6d037047b | ||
|
|
b5734c2b20 | ||
|
|
723fb7eaac | ||
|
|
63a9acaa19 | ||
|
|
0524f8c677 | ||
|
|
70b62f272e | ||
|
|
f0089b7940 | ||
|
|
4b44280d53 | ||
|
|
f045382d20 | ||
|
|
db3fa1ade7 | ||
|
|
f83950fd75 | ||
|
|
4dd1bf920d | ||
|
|
98755f3621 | ||
|
|
c3a8a044b9 | ||
|
|
15b5ea43a7 | ||
|
|
ec683fc227 | ||
|
|
d4e65eb82a | ||
|
|
10c6601b0a | ||
|
|
73940bc1bd | ||
|
|
9b7fb829f9 | ||
|
|
c51d8c9021 | ||
|
|
d8a6dfe5ce | ||
|
|
5f7cef0b06 | ||
|
|
48ff2ffc68 | ||
|
|
b3b9ccd314 | ||
|
|
e63c7b483b | ||
|
|
f57980b069 | ||
|
|
7006aa0d2a | ||
|
|
8051c1ca99 | ||
|
|
a779592414 | ||
|
|
112215848d |
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -76,6 +76,8 @@ async def to_code(config):
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
if CONF_RESIZE in config:
|
||||
image.thumbnail(config[CONF_RESIZE])
|
||||
frame = image.convert("RGB")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -733,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_)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -38,8 +38,12 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void send_local_time();
|
||||
#endif
|
||||
void set_clock(uint8_t hour, uint8_t minute);
|
||||
void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
/** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */
|
||||
void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; }
|
||||
|
||||
/** Attempts to check for and apply firmware updates. */
|
||||
void upgrade_firmware();
|
||||
@@ -74,6 +78,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
traits.add_supported_custom_preset("LTD HT");
|
||||
} else {
|
||||
traits.add_supported_custom_preset("EXT HT");
|
||||
}
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
@@ -85,11 +94,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod
|
||||
|
||||
#ifdef USE_TIME
|
||||
void setup_time_();
|
||||
void send_local_time_();
|
||||
optional<time::RealTimeClock *> time_id_{};
|
||||
#endif
|
||||
|
||||
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
|
||||
BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT;
|
||||
|
||||
static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
|
||||
static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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_))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -322,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) {
|
||||
|
||||
@@ -151,7 +151,6 @@ class ESP32Camera : public Component, public EntityBase {
|
||||
|
||||
protected:
|
||||
/* internal methods */
|
||||
uint32_t hash_base() override;
|
||||
bool has_requested_image_() const;
|
||||
bool can_return_image_() const;
|
||||
|
||||
|
||||
@@ -152,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
|
||||
|
||||
@@ -136,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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
0
esphome/components/i2s_audio/__init__.py
Normal file
0
esphome/components/i2s_audio/__init__.py
Normal file
156
esphome/components/i2s_audio/i2s_audio_media_player.cpp
Normal file
156
esphome/components/i2s_audio/i2s_audio_media_player.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "i2s_audio_media_player.h"
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace i2s_audio {
|
||||
|
||||
static const char *const TAG = "audio";
|
||||
|
||||
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
if (call.get_media_url().has_value()) {
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->stopSong();
|
||||
this->high_freq_.start();
|
||||
this->audio_->connecttohost(call.get_media_url().value().c_str());
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
}
|
||||
if (call.get_volume().has_value()) {
|
||||
this->volume = call.get_volume().value();
|
||||
this->set_volume_(volume);
|
||||
this->unmute_();
|
||||
}
|
||||
if (call.get_command().has_value()) {
|
||||
switch (call.get_command().value()) {
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
if (!this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_STOP:
|
||||
this->stop_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
|
||||
this->mute_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
this->unmute_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
this->audio_->pauseResume();
|
||||
if (this->audio_->isRunning()) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
} else {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
}
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
|
||||
float new_volume = this->volume + 0.1f;
|
||||
if (new_volume > 1.0f)
|
||||
new_volume = 1.0f;
|
||||
this->set_volume_(new_volume);
|
||||
this->unmute_();
|
||||
break;
|
||||
}
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: {
|
||||
float new_volume = this->volume - 0.1f;
|
||||
if (new_volume < 0.0f)
|
||||
new_volume = 0.0f;
|
||||
this->set_volume_(new_volume);
|
||||
this->unmute_();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::mute_() {
|
||||
if (this->mute_pin_ != nullptr) {
|
||||
this->mute_pin_->digital_write(true);
|
||||
} else {
|
||||
this->set_volume_(0.0f, false);
|
||||
}
|
||||
this->muted_ = true;
|
||||
}
|
||||
void I2SAudioMediaPlayer::unmute_() {
|
||||
if (this->mute_pin_ != nullptr) {
|
||||
this->mute_pin_->digital_write(false);
|
||||
} else {
|
||||
this->set_volume_(this->volume, false);
|
||||
}
|
||||
this->muted_ = false;
|
||||
}
|
||||
void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) {
|
||||
this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21));
|
||||
if (publish)
|
||||
this->volume = volume;
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::stop_() {
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->stopSong();
|
||||
this->high_freq_.stop();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Audio...");
|
||||
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
|
||||
this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_);
|
||||
} else {
|
||||
this->audio_ = make_unique<Audio>(false);
|
||||
this->audio_->setPinout(this->bclk_pin_, this->lrclk_pin_, this->dout_pin_);
|
||||
this->audio_->forceMono(this->external_dac_channels_ == 1);
|
||||
}
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
}
|
||||
|
||||
void I2SAudioMediaPlayer::loop() {
|
||||
this->audio_->loop();
|
||||
if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) {
|
||||
this->stop_();
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
|
||||
auto traits = media_player::MediaPlayerTraits();
|
||||
traits.set_supports_pause(true);
|
||||
return traits;
|
||||
};
|
||||
|
||||
void I2SAudioMediaPlayer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Audio:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGCONFIG(TAG, "Audio failed to initialize!");
|
||||
return;
|
||||
}
|
||||
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
|
||||
switch (this->internal_dac_mode_) {
|
||||
case I2S_DAC_CHANNEL_LEFT_EN:
|
||||
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left");
|
||||
break;
|
||||
case I2S_DAC_CHANNEL_RIGHT_EN:
|
||||
ESP_LOGCONFIG(TAG, " Internal DAC mode: Right");
|
||||
break;
|
||||
case I2S_DAC_CHANNEL_BOTH_EN:
|
||||
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left & Right");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace i2s_audio
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
63
esphome/components/i2s_audio/i2s_audio_media_player.h
Normal file
63
esphome/components/i2s_audio/i2s_audio_media_player.h
Normal 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
|
||||
94
esphome/components/i2s_audio/media_player.py
Normal file
94
esphome/components/i2s_audio/media_player.py
Normal 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")
|
||||
@@ -24,6 +24,7 @@ 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")
|
||||
@@ -31,6 +32,7 @@ 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="_")
|
||||
@@ -60,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)
|
||||
|
||||
|
||||
@@ -263,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
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace ili9341 {
|
||||
enum ILI9341Model {
|
||||
M5STACK = 0,
|
||||
TFT_24,
|
||||
TFT_24R,
|
||||
};
|
||||
|
||||
enum ILI9341ColorMode {
|
||||
@@ -48,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_();
|
||||
@@ -100,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
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
104
esphome/components/media_player/__init__.py
Normal file
104
esphome/components/media_player/__init__.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
|
||||
from esphome.automation import maybe_simple_id
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
media_player_ns = cg.esphome_ns.namespace("media_player")
|
||||
|
||||
MediaPlayer = media_player_ns.class_("MediaPlayer")
|
||||
|
||||
PlayAction = media_player_ns.class_(
|
||||
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
ToggleAction = media_player_ns.class_(
|
||||
"ToggleAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
PauseAction = media_player_ns.class_(
|
||||
"PauseAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
StopAction = media_player_ns.class_(
|
||||
"StopAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
VolumeUpAction = media_player_ns.class_(
|
||||
"VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
VolumeDownAction = media_player_ns.class_(
|
||||
"VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
VolumeSetAction = media_player_ns.class_(
|
||||
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
|
||||
)
|
||||
|
||||
CONF_VOLUME = "volume"
|
||||
|
||||
|
||||
async def setup_media_player_core_(var, config):
|
||||
await setup_entity(var, config)
|
||||
|
||||
|
||||
async def register_media_player(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_media_player(var))
|
||||
await setup_media_player_core_(var, config)
|
||||
|
||||
|
||||
MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.Schema({}))
|
||||
|
||||
|
||||
MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)})
|
||||
|
||||
|
||||
@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA)
|
||||
@automation.register_action(
|
||||
"media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA
|
||||
)
|
||||
async def media_player_action(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"media_player.volume_set",
|
||||
VolumeSetAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(MediaPlayer),
|
||||
cv.Required(CONF_VOLUME): cv.templatable(cv.percentage),
|
||||
},
|
||||
key=CONF_VOLUME,
|
||||
),
|
||||
)
|
||||
async def media_player_volume_set_action(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
volume = await cg.templatable(config[CONF_VOLUME], args, float)
|
||||
cg.add(var.set_volume(volume))
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_global(media_player_ns.using)
|
||||
cg.add_define("USE_MEDIA_PLAYER")
|
||||
30
esphome/components/media_player/automation.h
Normal file
30
esphome/components/media_player/automation.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "media_player.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace media_player {
|
||||
|
||||
#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \
|
||||
template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<MediaPlayer> { \
|
||||
void play(Ts... x) override { \
|
||||
this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \
|
||||
} \
|
||||
};
|
||||
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP)
|
||||
MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN)
|
||||
|
||||
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> {
|
||||
TEMPLATABLE_VALUE(float, volume)
|
||||
void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); }
|
||||
};
|
||||
|
||||
} // namespace media_player
|
||||
} // namespace esphome
|
||||
118
esphome/components/media_player/media_player.cpp
Normal file
118
esphome/components/media_player/media_player.cpp
Normal 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
|
||||
93
esphome/components/media_player/media_player.h
Normal file
93
esphome/components/media_player/media_player.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace media_player {
|
||||
|
||||
enum MediaPlayerState : uint8_t {
|
||||
MEDIA_PLAYER_STATE_NONE = 0,
|
||||
MEDIA_PLAYER_STATE_IDLE = 1,
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2,
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3
|
||||
};
|
||||
const char *media_player_state_to_string(MediaPlayerState state);
|
||||
|
||||
enum MediaPlayerCommand : uint8_t {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0,
|
||||
MEDIA_PLAYER_COMMAND_PAUSE = 1,
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2,
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3,
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
|
||||
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
|
||||
};
|
||||
const char *media_player_command_to_string(MediaPlayerCommand command);
|
||||
|
||||
class MediaPlayer;
|
||||
|
||||
class MediaPlayerTraits {
|
||||
public:
|
||||
MediaPlayerTraits() = default;
|
||||
|
||||
void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; }
|
||||
|
||||
bool get_supports_pause() const { return this->supports_pause_; }
|
||||
|
||||
protected:
|
||||
bool supports_pause_{false};
|
||||
};
|
||||
|
||||
class MediaPlayerCall {
|
||||
public:
|
||||
MediaPlayerCall(MediaPlayer *parent) : parent_(parent) {}
|
||||
|
||||
MediaPlayerCall &set_command(MediaPlayerCommand command);
|
||||
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
|
||||
MediaPlayerCall &set_command(const std::string &command);
|
||||
|
||||
MediaPlayerCall &set_media_url(const std::string &url);
|
||||
|
||||
MediaPlayerCall &set_volume(float volume);
|
||||
|
||||
void perform();
|
||||
|
||||
const optional<MediaPlayerCommand> &get_command() const { return command_; }
|
||||
const optional<std::string> &get_media_url() const { return media_url_; }
|
||||
const optional<float> &get_volume() const { return volume_; }
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
MediaPlayer *const parent_;
|
||||
optional<MediaPlayerCommand> command_;
|
||||
optional<std::string> media_url_;
|
||||
optional<float> volume_;
|
||||
};
|
||||
|
||||
class MediaPlayer : public EntityBase {
|
||||
public:
|
||||
MediaPlayerState state{MEDIA_PLAYER_STATE_NONE};
|
||||
float volume{1.0f};
|
||||
|
||||
MediaPlayerCall make_call() { return MediaPlayerCall(this); }
|
||||
|
||||
void publish_state();
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
virtual bool is_muted() const { return false; }
|
||||
|
||||
virtual MediaPlayerTraits get_traits() = 0;
|
||||
|
||||
protected:
|
||||
friend MediaPlayerCall;
|
||||
|
||||
virtual void control(const MediaPlayerCall &call) = 0;
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
};
|
||||
|
||||
} // namespace media_player
|
||||
} // namespace esphome
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -114,6 +114,7 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
|
||||
if (this->state_->get_traits().supports_speed()) {
|
||||
root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic();
|
||||
root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic();
|
||||
root[MQTT_SPEED_RANGE_MAX] = this->state_->get_traits().supported_speed_count();
|
||||
}
|
||||
}
|
||||
bool MQTTFanComponent::publish_state() {
|
||||
|
||||
@@ -115,7 +115,7 @@ void Nextion::set_backlight_brightness(float brightness) {
|
||||
ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0");
|
||||
return;
|
||||
}
|
||||
this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast<int>(brightness * 100));
|
||||
this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100));
|
||||
}
|
||||
|
||||
void Nextion::set_auto_wake_on_touch(bool auto_wake) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
181
esphome/components/remote_base/rc6_protocol.cpp
Normal file
181
esphome/components/remote_base/rc6_protocol.cpp
Normal 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
|
||||
46
esphome/components/remote_base/rc6_protocol.h
Normal file
46
esphome/components/remote_base/rc6_protocol.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -58,7 +58,5 @@ optional<std::string> Select::at(size_t index) const {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Select::hash_base() { return 2812997003UL; }
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
||||
@@ -30,6 +30,8 @@ class SSD1322 : public PollingComponent, public display::DisplayBuffer {
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void data(uint8_t value) = 0;
|
||||
|
||||
@@ -35,6 +35,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer {
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
||||
@@ -30,6 +30,8 @@ class SSD1327 : public PollingComponent, public display::DisplayBuffer {
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
||||
@@ -25,6 +25,8 @@ class SSD1331 : public PollingComponent, public display::DisplayBuffer {
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void write_display_data() = 0;
|
||||
|
||||
@@ -31,6 +31,8 @@ class SSD1351 : public PollingComponent, public display::DisplayBuffer {
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void fill(Color color) override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
virtual void command(uint8_t value) = 0;
|
||||
virtual void data(uint8_t value) = 0;
|
||||
|
||||
@@ -53,6 +53,8 @@ class ST7735 : public PollingComponent,
|
||||
void set_dc_pin(GPIOPin *value) { dc_pin_ = value; }
|
||||
size_t get_buffer_length();
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes);
|
||||
void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes);
|
||||
|
||||
@@ -126,6 +126,8 @@ class ST7789V : public PollingComponent,
|
||||
|
||||
void write_display_data();
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
protected:
|
||||
GPIOPin *dc_pin_;
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
|
||||
@@ -29,6 +29,8 @@ class ST7920 : public PollingComponent,
|
||||
void fill(Color color) override;
|
||||
void write_display_data();
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
int get_height_internal() override;
|
||||
|
||||
@@ -48,7 +48,7 @@ VARIABLE_PROG = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _expand_substitutions(substitutions, value, path):
|
||||
def _expand_substitutions(substitutions, value, path, ignore_missing):
|
||||
if "$" not in value:
|
||||
return value
|
||||
|
||||
@@ -66,13 +66,14 @@ def _expand_substitutions(substitutions, value, path):
|
||||
if name.startswith("{") and name.endswith("}"):
|
||||
name = name[1:-1]
|
||||
if name not in substitutions:
|
||||
_LOGGER.warning(
|
||||
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
|
||||
"not declared",
|
||||
orig_value,
|
||||
"->".join(str(x) for x in path),
|
||||
name,
|
||||
)
|
||||
if not ignore_missing:
|
||||
_LOGGER.warning(
|
||||
"Found '%s' (see %s) which looks like a substitution, but '%s' was "
|
||||
"not declared",
|
||||
orig_value,
|
||||
"->".join(str(x) for x in path),
|
||||
name,
|
||||
)
|
||||
i = j
|
||||
continue
|
||||
|
||||
@@ -92,37 +93,37 @@ def _expand_substitutions(substitutions, value, path):
|
||||
return value
|
||||
|
||||
|
||||
def _substitute_item(substitutions, item, path):
|
||||
def _substitute_item(substitutions, item, path, ignore_missing):
|
||||
if isinstance(item, list):
|
||||
for i, it in enumerate(item):
|
||||
sub = _substitute_item(substitutions, it, path + [i])
|
||||
sub = _substitute_item(substitutions, it, path + [i], ignore_missing)
|
||||
if sub is not None:
|
||||
item[i] = sub
|
||||
elif isinstance(item, dict):
|
||||
replace_keys = []
|
||||
for k, v in item.items():
|
||||
if path or k != CONF_SUBSTITUTIONS:
|
||||
sub = _substitute_item(substitutions, k, path + [k])
|
||||
sub = _substitute_item(substitutions, k, path + [k], ignore_missing)
|
||||
if sub is not None:
|
||||
replace_keys.append((k, sub))
|
||||
sub = _substitute_item(substitutions, v, path + [k])
|
||||
sub = _substitute_item(substitutions, v, path + [k], ignore_missing)
|
||||
if sub is not None:
|
||||
item[k] = sub
|
||||
for old, new in replace_keys:
|
||||
item[new] = merge_config(item.get(old), item.get(new))
|
||||
del item[old]
|
||||
elif isinstance(item, str):
|
||||
sub = _expand_substitutions(substitutions, item, path)
|
||||
sub = _expand_substitutions(substitutions, item, path, ignore_missing)
|
||||
if sub != item:
|
||||
return sub
|
||||
elif isinstance(item, core.Lambda):
|
||||
sub = _expand_substitutions(substitutions, item.value, path)
|
||||
sub = _expand_substitutions(substitutions, item.value, path, ignore_missing)
|
||||
if sub != item:
|
||||
item.value = sub
|
||||
return None
|
||||
|
||||
|
||||
def do_substitution_pass(config, command_line_substitutions):
|
||||
def do_substitution_pass(config, command_line_substitutions, ignore_missing=False):
|
||||
if CONF_SUBSTITUTIONS not in config and not command_line_substitutions:
|
||||
return
|
||||
|
||||
@@ -151,4 +152,4 @@ def do_substitution_pass(config, command_line_substitutions):
|
||||
config[CONF_SUBSTITUTIONS] = substitutions
|
||||
# Move substitutions to the first place to replace substitutions in them correctly
|
||||
config.move_to_end(CONF_SUBSTITUTIONS, False)
|
||||
_substitute_item(substitutions, config, [])
|
||||
_substitute_item(substitutions, config, [], ignore_missing)
|
||||
|
||||
@@ -43,7 +43,6 @@ void Switch::add_on_state_callback(std::function<void(bool)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||
uint32_t Switch::hash_base() { return 3129890955UL; }
|
||||
bool Switch::is_inverted() const { return this->inverted_; }
|
||||
|
||||
std::string Switch::get_device_class() {
|
||||
|
||||
@@ -107,8 +107,6 @@ class Switch : public EntityBase {
|
||||
*/
|
||||
virtual void write_state(bool state) = 0;
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(bool)> state_callback_{};
|
||||
bool inverted_{false};
|
||||
Deduplicator<bool> publish_dedup_;
|
||||
|
||||
@@ -70,7 +70,6 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
|
||||
|
||||
std::string TextSensor::unique_id() { return ""; }
|
||||
bool TextSensor::has_state() { return this->has_state_; }
|
||||
uint32_t TextSensor::hash_base() { return 334300109UL; }
|
||||
|
||||
} // namespace text_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -59,8 +59,6 @@ class TextSensor : public EntityBase {
|
||||
void internal_send_state_to_frontend(const std::string &state);
|
||||
|
||||
protected:
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(std::string)> raw_callback_; ///< Storage for raw state callbacks.
|
||||
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
|
||||
CONF_DRY_ACTION,
|
||||
CONF_DRY_MODE,
|
||||
CONF_FAN_MODE,
|
||||
CONF_FAN_MODE_ON_ACTION,
|
||||
CONF_FAN_MODE_OFF_ACTION,
|
||||
CONF_FAN_MODE_AUTO_ACTION,
|
||||
@@ -37,6 +38,7 @@ from esphome.const import (
|
||||
CONF_IDLE_ACTION,
|
||||
CONF_MAX_COOLING_RUN_TIME,
|
||||
CONF_MAX_HEATING_RUN_TIME,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_COOLING_OFF_TIME,
|
||||
CONF_MIN_COOLING_RUN_TIME,
|
||||
CONF_MIN_FAN_MODE_SWITCHING_TIME,
|
||||
@@ -45,7 +47,11 @@ from esphome.const import (
|
||||
CONF_MIN_HEATING_OFF_TIME,
|
||||
CONF_MIN_HEATING_RUN_TIME,
|
||||
CONF_MIN_IDLE_TIME,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_NAME,
|
||||
CONF_MODE,
|
||||
CONF_OFF_MODE,
|
||||
CONF_PRESET,
|
||||
CONF_SENSOR,
|
||||
CONF_SET_POINT_MINIMUM_DIFFERENTIAL,
|
||||
CONF_STARTUP_DELAY,
|
||||
@@ -55,11 +61,15 @@ from esphome.const import (
|
||||
CONF_SUPPLEMENTAL_HEATING_DELTA,
|
||||
CONF_SWING_BOTH_ACTION,
|
||||
CONF_SWING_HORIZONTAL_ACTION,
|
||||
CONF_SWING_MODE,
|
||||
CONF_SWING_OFF_ACTION,
|
||||
CONF_SWING_VERTICAL_ACTION,
|
||||
CONF_TARGET_TEMPERATURE_CHANGE_ACTION,
|
||||
CONF_VISUAL,
|
||||
)
|
||||
|
||||
CONF_PRESET_CHANGE = "preset_change"
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
|
||||
climate_ns = cg.esphome_ns.namespace("climate")
|
||||
@@ -82,6 +92,38 @@ CLIMATE_MODES = {
|
||||
}
|
||||
validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True)
|
||||
|
||||
ClimatePreset = climate_ns.enum("ClimatePreset")
|
||||
|
||||
PRESET_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ThermostatClimateTargetTempConfig),
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Optional(CONF_MODE): validate_climate_mode,
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode),
|
||||
cv.Optional(CONF_SWING_MODE): cv.templatable(
|
||||
climate.validate_climate_swing_mode
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_temperature_preset(preset, root_config, name, requirements):
|
||||
# verify temperature settings for the provided preset / default / away configuration
|
||||
for config_temp, req_actions in requirements.items():
|
||||
for req_action in req_actions:
|
||||
# verify corresponding default target temperature exists when a given climate action exists
|
||||
if config_temp not in preset and req_action in root_config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} must be defined in {name} config when using {req_action}"
|
||||
)
|
||||
# if a given climate action is NOT defined, it should not have a default target temperature
|
||||
if config_temp in preset and req_action not in root_config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} is defined in {name} config with no {req_action}"
|
||||
)
|
||||
|
||||
|
||||
def validate_thermostat(config):
|
||||
# verify corresponding action(s) exist(s) for any defined climate mode or action
|
||||
@@ -235,33 +277,22 @@ def validate_thermostat(config):
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION],
|
||||
}
|
||||
|
||||
for config_temp, req_actions in requirements.items():
|
||||
for req_action in req_actions:
|
||||
# verify corresponding default target temperature exists when a given climate action exists
|
||||
if config_temp not in config and req_action in config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} must be defined when using {req_action}"
|
||||
)
|
||||
# if a given climate action is NOT defined, it should not have a default target temperature
|
||||
if config_temp in config and req_action not in config:
|
||||
raise cv.Invalid(f"{config_temp} is defined with no {req_action}")
|
||||
# Validate temperature requirements for default configuraation
|
||||
validate_temperature_preset(config, config, "default", requirements)
|
||||
|
||||
# Validate temperature requirements for away configuration
|
||||
if CONF_AWAY_CONFIG in config:
|
||||
away = config[CONF_AWAY_CONFIG]
|
||||
for config_temp, req_actions in requirements.items():
|
||||
for req_action in req_actions:
|
||||
# verify corresponding default target temperature exists when a given climate action exists
|
||||
if config_temp not in away and req_action in config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} must be defined in away configuration when using {req_action}"
|
||||
)
|
||||
# if a given climate action is NOT defined, it should not have a default target temperature
|
||||
if config_temp in away and req_action not in config:
|
||||
raise cv.Invalid(
|
||||
f"{config_temp} is defined in away configuration with no {req_action}"
|
||||
)
|
||||
validate_temperature_preset(away, config, "away", requirements)
|
||||
|
||||
# verify default climate mode is valid given above configuration
|
||||
# Validate temperature requirements for presets
|
||||
if CONF_PRESET in config:
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
validate_temperature_preset(
|
||||
preset_config, config, preset_config[CONF_NAME], requirements
|
||||
)
|
||||
|
||||
# Verify default climate mode is valid given above configuration
|
||||
default_mode = config[CONF_DEFAULT_MODE]
|
||||
requirements = {
|
||||
"HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
|
||||
@@ -270,13 +301,108 @@ def validate_thermostat(config):
|
||||
"DRY": [CONF_DRY_ACTION],
|
||||
"FAN_ONLY": [CONF_FAN_ONLY_ACTION],
|
||||
"AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
|
||||
}.get(default_mode, [])
|
||||
for req in requirements:
|
||||
"OFF": [],
|
||||
}
|
||||
actions_for_default_mode = requirements.get(default_mode, [])
|
||||
for req in actions_for_default_mode:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
# Verify that the modes for presets are valid given the configuration
|
||||
if CONF_PRESET in config:
|
||||
# Preset temperature vs Visual temperature validation
|
||||
|
||||
# Default visual configuration from climate_traits.h
|
||||
visual_min_temperature = 10.0
|
||||
visual_max_temperature = 30.0
|
||||
if CONF_VISUAL in config:
|
||||
visual_config = config[CONF_VISUAL]
|
||||
|
||||
if CONF_MIN_TEMPERATURE in visual_config:
|
||||
visual_min_temperature = visual_config[CONF_MIN_TEMPERATURE]
|
||||
|
||||
if CONF_MAX_TEMPERATURE in visual_config:
|
||||
visual_max_temperature = visual_config[CONF_MAX_TEMPERATURE]
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config:
|
||||
preset_min_temperature = preset_config[
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW
|
||||
]
|
||||
if preset_min_temperature < visual_min_temperature:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} for {preset_config[CONF_NAME]} is set to {preset_min_temperature} which is less than the visual minimum temperature of {visual_min_temperature}"
|
||||
)
|
||||
|
||||
if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config:
|
||||
preset_max_temperature = preset_config[
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH
|
||||
]
|
||||
if preset_max_temperature > visual_max_temperature:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} for {preset_config[CONF_NAME]} is set to {preset_max_temperature} which is more than the visual maximum temperature of {visual_max_temperature}"
|
||||
)
|
||||
|
||||
# Mode validation
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_MODE not in preset_config:
|
||||
continue
|
||||
|
||||
mode = preset_config[CONF_MODE]
|
||||
|
||||
for req in requirements[mode]:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_MODE} is set to {mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
# Fan mode requirements
|
||||
requirements = {
|
||||
"ON": [CONF_FAN_MODE_ON_ACTION],
|
||||
"OFF": [CONF_FAN_MODE_OFF_ACTION],
|
||||
"AUTO": [CONF_FAN_MODE_AUTO_ACTION],
|
||||
"LOW": [CONF_FAN_MODE_LOW_ACTION],
|
||||
"MEDIUM": [CONF_FAN_MODE_MEDIUM_ACTION],
|
||||
"HIGH": [CONF_FAN_MODE_HIGH_ACTION],
|
||||
"MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION],
|
||||
"FOCUS": [CONF_FAN_MODE_FOCUS_ACTION],
|
||||
"DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION],
|
||||
}
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_FAN_MODE not in preset_config:
|
||||
continue
|
||||
|
||||
fan_mode = preset_config[CONF_FAN_MODE]
|
||||
|
||||
for req in requirements[fan_mode]:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_FAN_MODE} is set to {fan_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
# Swing mode requirements
|
||||
requirements = {
|
||||
"OFF": [CONF_SWING_OFF_ACTION],
|
||||
"BOTH": [CONF_SWING_BOTH_ACTION],
|
||||
"VERTICAL": [CONF_SWING_VERTICAL_ACTION],
|
||||
"HORIZONTAL": [CONF_SWING_HORIZONTAL_ACTION],
|
||||
}
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
if CONF_SWING_MODE not in preset_config:
|
||||
continue
|
||||
|
||||
swing_mode = preset_config[CONF_SWING_MODE]
|
||||
|
||||
for req in requirements[swing_mode]:
|
||||
if req not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration"
|
||||
)
|
||||
|
||||
if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}"
|
||||
@@ -415,6 +541,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA),
|
||||
cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(
|
||||
@@ -531,7 +661,7 @@ async def to_code(config):
|
||||
cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING]))
|
||||
|
||||
cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY]))
|
||||
cg.add(var.set_normal_config(normal_config))
|
||||
cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config))
|
||||
|
||||
await automation.build_automation(
|
||||
var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION]
|
||||
@@ -694,4 +824,55 @@ async def to_code(config):
|
||||
away_config = ThermostatClimateTargetTempConfig(
|
||||
away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
|
||||
)
|
||||
cg.add(var.set_away_config(away_config))
|
||||
cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config))
|
||||
|
||||
if CONF_PRESET in config:
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
|
||||
name = preset_config[CONF_NAME]
|
||||
standard_preset = None
|
||||
if name.upper() in climate.CLIMATE_PRESETS:
|
||||
standard_preset = climate.CLIMATE_PRESETS[name.upper()]
|
||||
|
||||
if two_points_available is True:
|
||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
|
||||
)
|
||||
elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config:
|
||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
|
||||
)
|
||||
elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config:
|
||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
|
||||
)
|
||||
|
||||
preset_target_variable = cg.new_variable(
|
||||
preset_config[CONF_ID], preset_target_config
|
||||
)
|
||||
|
||||
if CONF_MODE in preset_config:
|
||||
cg.add(preset_target_variable.set_mode(preset_config[CONF_MODE]))
|
||||
|
||||
if CONF_FAN_MODE in preset_config:
|
||||
cg.add(
|
||||
preset_target_variable.set_fan_mode(preset_config[CONF_FAN_MODE])
|
||||
)
|
||||
|
||||
if CONF_SWING_MODE in preset_config:
|
||||
cg.add(
|
||||
preset_target_variable.set_swing_mode(
|
||||
preset_config[CONF_SWING_MODE]
|
||||
)
|
||||
)
|
||||
|
||||
if standard_preset is not None:
|
||||
cg.add(var.set_preset_config(standard_preset, preset_target_variable))
|
||||
else:
|
||||
cg.add(var.set_custom_preset_config(name, preset_target_variable))
|
||||
|
||||
if CONF_PRESET_CHANGE in config:
|
||||
await automation.build_automation(
|
||||
var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE]
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ void ThermostatClimate::setup() {
|
||||
} else {
|
||||
// restore from defaults, change_away handles temps for us
|
||||
this->mode = this->default_mode_;
|
||||
this->change_away_(false);
|
||||
this->change_preset_(climate::CLIMATE_PRESET_HOME);
|
||||
}
|
||||
// refresh the climate action based on the restored settings, we'll publish_state() later
|
||||
this->switch_to_action_(this->compute_action_(), false);
|
||||
@@ -162,11 +162,20 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
|
||||
if (call.get_preset().has_value()) {
|
||||
// setup_complete_ blocks modifying/resetting the temps immediately after boot
|
||||
if (this->setup_complete_) {
|
||||
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
||||
this->change_preset_(*call.get_preset());
|
||||
} else {
|
||||
this->preset = *call.get_preset();
|
||||
}
|
||||
}
|
||||
if (call.get_custom_preset().has_value()) {
|
||||
// setup_complete_ blocks modifying/resetting the temps immediately after boot
|
||||
if (this->setup_complete_) {
|
||||
this->change_custom_preset_(*call.get_custom_preset());
|
||||
} else {
|
||||
this->custom_preset = *call.get_custom_preset();
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_mode().has_value())
|
||||
this->mode = *call.get_mode();
|
||||
if (call.get_fan_mode().has_value())
|
||||
@@ -236,8 +245,12 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
||||
if (supports_swing_mode_vertical_)
|
||||
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
|
||||
|
||||
if (supports_away_)
|
||||
traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY});
|
||||
for (auto &it : this->preset_config_) {
|
||||
traits.add_supported_preset(it.first);
|
||||
}
|
||||
for (auto &it : this->custom_preset_config_) {
|
||||
traits.add_supported_custom_preset(it.first);
|
||||
}
|
||||
|
||||
traits.set_supports_two_point_target_temperature(this->supports_two_points_);
|
||||
traits.set_supports_action(true);
|
||||
@@ -910,30 +923,112 @@ bool ThermostatClimate::supplemental_heating_required_() {
|
||||
(this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING));
|
||||
}
|
||||
|
||||
void ThermostatClimate::change_away_(bool away) {
|
||||
if (!away) {
|
||||
void ThermostatClimate::dump_preset_config_(const std::string &preset,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
const auto *preset_name = preset.c_str();
|
||||
|
||||
if (this->supports_heat_) {
|
||||
if (this->supports_two_points_) {
|
||||
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
||||
this->target_temperature_high = this->normal_config_.default_temperature_high;
|
||||
} else
|
||||
this->target_temperature = this->normal_config_.default_temperature;
|
||||
} else {
|
||||
if (this->supports_two_points_) {
|
||||
this->target_temperature_low = this->away_config_.default_temperature_low;
|
||||
this->target_temperature_high = this->away_config_.default_temperature_high;
|
||||
} else
|
||||
this->target_temperature = this->away_config_.default_temperature;
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name,
|
||||
config.default_temperature_low);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature);
|
||||
}
|
||||
}
|
||||
if ((this->supports_cool_) || (this->supports_fan_only_)) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name,
|
||||
config.default_temperature_high);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.mode_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name,
|
||||
LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
|
||||
}
|
||||
if (config.fan_mode_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name,
|
||||
LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
|
||||
}
|
||||
if (config.swing_mode_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name,
|
||||
LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
|
||||
}
|
||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) {
|
||||
this->normal_config_ = normal_config;
|
||||
void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
|
||||
auto config = this->preset_config_.find(preset);
|
||||
|
||||
if (config != this->preset_config_.end()) {
|
||||
ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
|
||||
this->change_preset_internal_(config->second);
|
||||
|
||||
this->custom_preset.reset();
|
||||
this->preset = preset;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) {
|
||||
this->supports_away_ = true;
|
||||
this->away_config_ = away_config;
|
||||
void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) {
|
||||
auto config = this->custom_preset_config_.find(custom_preset);
|
||||
|
||||
if (config != this->custom_preset_config_.end()) {
|
||||
ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str());
|
||||
this->change_preset_internal_(config->second);
|
||||
|
||||
this->preset.reset();
|
||||
this->custom_preset = custom_preset;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) {
|
||||
if (this->supports_two_points_) {
|
||||
this->target_temperature_low = config.default_temperature_low;
|
||||
this->target_temperature_high = config.default_temperature_high;
|
||||
} else {
|
||||
this->target_temperature = config.default_temperature;
|
||||
}
|
||||
|
||||
// Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call
|
||||
// also specifies them then the control's version will override these for that call
|
||||
if (config.mode_.has_value()) {
|
||||
this->mode = *config.mode_;
|
||||
ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
|
||||
}
|
||||
|
||||
if (config.fan_mode_.has_value()) {
|
||||
this->fan_mode = *config.fan_mode_;
|
||||
ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
|
||||
}
|
||||
|
||||
if (config.swing_mode_.has_value()) {
|
||||
ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
|
||||
this->swing_mode = *config.swing_mode_;
|
||||
}
|
||||
|
||||
// Fire any preset changed trigger if defined
|
||||
if (this->preset != preset) {
|
||||
Trigger<> *trig = this->preset_change_trigger_;
|
||||
assert(trig != nullptr);
|
||||
trig->trigger();
|
||||
}
|
||||
|
||||
this->refresh();
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_preset_config(climate::ClimatePreset preset,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
this->preset_config_[preset] = config;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_custom_preset_config(const std::string &name,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
this->custom_preset_config_[name] = config;
|
||||
}
|
||||
|
||||
ThermostatClimate::ThermostatClimate()
|
||||
@@ -963,7 +1058,8 @@ ThermostatClimate::ThermostatClimate()
|
||||
swing_mode_off_trigger_(new Trigger<>()),
|
||||
swing_mode_horizontal_trigger_(new Trigger<>()),
|
||||
swing_mode_vertical_trigger_(new Trigger<>()),
|
||||
temperature_change_trigger_(new Trigger<>()) {}
|
||||
temperature_change_trigger_(new Trigger<>()),
|
||||
preset_change_trigger_(new Trigger<>()) {}
|
||||
|
||||
void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; }
|
||||
void ThermostatClimate::set_set_point_minimum_differential(float differential) {
|
||||
@@ -1112,23 +1208,11 @@ Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; }
|
||||
|
||||
void ThermostatClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Thermostat", this);
|
||||
if (this->supports_heat_) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->supports_two_points_)
|
||||
ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_);
|
||||
ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_));
|
||||
@@ -1194,24 +1278,21 @@ void ThermostatClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_));
|
||||
ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_));
|
||||
ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_));
|
||||
ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_));
|
||||
if (this->supports_away_) {
|
||||
if (this->supports_heat_) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C",
|
||||
this->away_config_.default_temperature_low);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
if ((this->supports_cool_) || (this->supports_fan_only_)) {
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C",
|
||||
this->away_config_.default_temperature_high);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported PRESETS: ");
|
||||
for (auto &it : this->preset_config_) {
|
||||
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first));
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true));
|
||||
this->dump_preset_config_(preset_name, it.second);
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: ");
|
||||
for (auto &it : this->custom_preset_config_) {
|
||||
const auto *preset_name = it.first.c_str();
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true));
|
||||
this->dump_preset_config_(preset_name, it.second);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include <map>
|
||||
|
||||
namespace esphome {
|
||||
namespace thermostat {
|
||||
@@ -34,6 +35,10 @@ struct ThermostatClimateTargetTempConfig {
|
||||
ThermostatClimateTargetTempConfig(float default_temperature);
|
||||
ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high);
|
||||
|
||||
void set_fan_mode(climate::ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; }
|
||||
void set_swing_mode(climate::ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; }
|
||||
void set_mode(climate::ClimateMode mode) { this->mode_ = mode; }
|
||||
|
||||
float default_temperature{NAN};
|
||||
float default_temperature_low{NAN};
|
||||
float default_temperature_high{NAN};
|
||||
@@ -41,6 +46,9 @@ struct ThermostatClimateTargetTempConfig {
|
||||
float cool_overrun_{NAN};
|
||||
float heat_deadband_{NAN};
|
||||
float heat_overrun_{NAN};
|
||||
optional<climate::ClimateFanMode> fan_mode_{};
|
||||
optional<climate::ClimateSwingMode> swing_mode_{};
|
||||
optional<climate::ClimateMode> mode_{};
|
||||
};
|
||||
|
||||
class ThermostatClimate : public climate::Climate, public Component {
|
||||
@@ -94,8 +102,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
|
||||
void set_supports_two_points(bool supports_two_points);
|
||||
|
||||
void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const ThermostatClimateTargetTempConfig &away_config);
|
||||
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
||||
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
Trigger<> *get_cool_action_trigger() const;
|
||||
Trigger<> *get_supplemental_cool_action_trigger() const;
|
||||
@@ -124,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
Trigger<> *get_swing_mode_off_trigger() const;
|
||||
Trigger<> *get_swing_mode_vertical_trigger() const;
|
||||
Trigger<> *get_temperature_change_trigger() const;
|
||||
Trigger<> *get_preset_change_trigger() const;
|
||||
/// Get current hysteresis values
|
||||
float cool_deadband();
|
||||
float cool_overrun();
|
||||
@@ -149,8 +158,14 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
/// Change the away setting, will reset target temperatures to defaults.
|
||||
void change_away_(bool away);
|
||||
/// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly
|
||||
void change_preset_(climate::ClimatePreset preset);
|
||||
/// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly
|
||||
void change_custom_preset_(const std::string &custom_preset);
|
||||
|
||||
/// Applies the temperature, mode, fan, and swing modes of the provded config.
|
||||
/// This is agnostic of custom vs built in preset
|
||||
void change_preset_internal_(const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
/// Return the traits of this controller.
|
||||
climate::ClimateTraits traits() override;
|
||||
@@ -210,6 +225,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
bool supplemental_cooling_required_();
|
||||
bool supplemental_heating_required_();
|
||||
|
||||
void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
/// The sensor used for getting the current temperature
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
|
||||
@@ -267,11 +284,6 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// A false value means that the controller has no such support.
|
||||
bool supports_two_points_{false};
|
||||
|
||||
/// Whether the controller supports an "away" mode
|
||||
///
|
||||
/// A false value means that the controller has no such mode.
|
||||
bool supports_away_{false};
|
||||
|
||||
/// Flags indicating if maximum allowable run time was exceeded
|
||||
bool cooling_max_runtime_exceeded_{false};
|
||||
bool heating_max_runtime_exceeded_{false};
|
||||
@@ -368,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// The trigger to call when the target temperature(s) change(es).
|
||||
Trigger<> *temperature_change_trigger_{nullptr};
|
||||
|
||||
/// The triggr to call when the preset mode changes
|
||||
Trigger<> *preset_change_trigger_{nullptr};
|
||||
|
||||
/// A reference to the trigger that was previously active.
|
||||
///
|
||||
/// This is so that the previous trigger can be stopped before enabling a new one
|
||||
@@ -409,10 +424,6 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// Minimum allowable duration in seconds for action timers
|
||||
const uint8_t min_timer_duration_{1};
|
||||
|
||||
/// Temperature data for normal/home and away modes
|
||||
ThermostatClimateTargetTempConfig normal_config_{};
|
||||
ThermostatClimateTargetTempConfig away_config_{};
|
||||
|
||||
/// Climate action timers
|
||||
std::vector<ThermostatClimateTimer> timer_{
|
||||
{"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)},
|
||||
@@ -425,6 +436,11 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
{"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)},
|
||||
{"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)},
|
||||
{"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}};
|
||||
|
||||
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
|
||||
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
|
||||
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
|
||||
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
|
||||
};
|
||||
|
||||
} // namespace thermostat
|
||||
|
||||
@@ -10,6 +10,11 @@ static const char *const TAG = "tuya.light";
|
||||
void TuyaLight::setup() {
|
||||
if (this->color_temperature_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->color_temperature_id_, [this](const TuyaDatapoint &datapoint) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto datapoint_value = datapoint.value_uint;
|
||||
if (this->color_temperature_invert_) {
|
||||
datapoint_value = this->color_temperature_max_value_ - datapoint_value;
|
||||
@@ -23,6 +28,11 @@ void TuyaLight::setup() {
|
||||
}
|
||||
if (this->dimmer_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->dimmer_id_, [this](const TuyaDatapoint &datapoint) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = this->state_->make_call();
|
||||
call.set_brightness(float(datapoint.value_uint) / this->max_value_);
|
||||
call.perform();
|
||||
@@ -30,6 +40,11 @@ void TuyaLight::setup() {
|
||||
}
|
||||
if (switch_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = this->state_->make_call();
|
||||
call.set_state(datapoint.value_bool);
|
||||
call.perform();
|
||||
@@ -41,6 +56,11 @@ void TuyaLight::setup() {
|
||||
auto green = parse_hex<uint8_t>(datapoint.value_string.substr(2, 2));
|
||||
auto blue = parse_hex<uint8_t>(datapoint.value_string.substr(4, 2));
|
||||
if (red.has_value() && green.has_value() && blue.has_value()) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = this->state_->make_call();
|
||||
call.set_rgb(float(*red) / 255, float(*green) / 255, float(*blue) / 255);
|
||||
call.perform();
|
||||
@@ -52,6 +72,11 @@ void TuyaLight::setup() {
|
||||
auto saturation = parse_hex<uint16_t>(datapoint.value_string.substr(4, 4));
|
||||
auto value = parse_hex<uint16_t>(datapoint.value_string.substr(8, 4));
|
||||
if (hue.has_value() && saturation.has_value() && value.has_value()) {
|
||||
if (this->state_->current_values != this->state_->remote_values) {
|
||||
ESP_LOGD(TAG, "Light is transitioning, datapoint change ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
float red, green, blue;
|
||||
hsv_to_rgb(*hue, float(*saturation) / 1000, float(*value) / 1000, red, green, blue);
|
||||
auto call = this->state_->make_call();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "tuya.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ class WaveshareEPaper : public PollingComponent,
|
||||
|
||||
void on_safe_shutdown() override;
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user