mirror of
https://github.com/esphome/esphome.git
synced 2024-12-26 07:24:54 +01:00
Merge branch 'dev' into hbridge-switch
This commit is contained in:
commit
275dd84948
32 changed files with 1729 additions and 72 deletions
|
@ -290,6 +290,7 @@ esphome/components/noblex/* @AGalfra
|
||||||
esphome/components/number/* @esphome/core
|
esphome/components/number/* @esphome/core
|
||||||
esphome/components/one_wire/* @ssieb
|
esphome/components/one_wire/* @ssieb
|
||||||
esphome/components/online_image/* @guillempages
|
esphome/components/online_image/* @guillempages
|
||||||
|
esphome/components/opentherm/* @olegtarasov
|
||||||
esphome/components/ota/* @esphome/core
|
esphome/components/ota/* @esphome/core
|
||||||
esphome/components/output/* @esphome/core
|
esphome/components/output/* @esphome/core
|
||||||
esphome/components/pca6416a/* @Mat931
|
esphome/components/pca6416a/* @Mat931
|
||||||
|
|
|
@ -33,7 +33,7 @@ RUN \
|
||||||
python3-venv=3.11.2-1+b1 \
|
python3-venv=3.11.2-1+b1 \
|
||||||
python3-wheel=0.38.4-2 \
|
python3-wheel=0.38.4-2 \
|
||||||
iputils-ping=3:20221126-1 \
|
iputils-ping=3:20221126-1 \
|
||||||
git=1:2.39.2-1.1 \
|
git=1:2.39.5-0+deb12u1 \
|
||||||
curl=7.88.1-10+deb12u7 \
|
curl=7.88.1-10+deb12u7 \
|
||||||
openssh-client=1:9.2p1-2+deb12u3 \
|
openssh-client=1:9.2p1-2+deb12u3 \
|
||||||
python3-cffi=1.15.1-5 \
|
python3-cffi=1.15.1-5 \
|
||||||
|
|
|
@ -1118,6 +1118,7 @@ message MediaPlayerSupportedFormat {
|
||||||
uint32 sample_rate = 2;
|
uint32 sample_rate = 2;
|
||||||
uint32 num_channels = 3;
|
uint32 num_channels = 3;
|
||||||
MediaPlayerFormatPurpose purpose = 4;
|
MediaPlayerFormatPurpose purpose = 4;
|
||||||
|
uint32 sample_bytes = 5;
|
||||||
}
|
}
|
||||||
message ListEntitiesMediaPlayerResponse {
|
message ListEntitiesMediaPlayerResponse {
|
||||||
option (id) = 63;
|
option (id) = 63;
|
||||||
|
@ -1570,6 +1571,36 @@ message VoiceAssistantAnnounceFinished {
|
||||||
bool success = 1;
|
bool success = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message VoiceAssistantWakeWord {
|
||||||
|
uint32 id = 1;
|
||||||
|
string wake_word = 2;
|
||||||
|
repeated string trained_languages = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VoiceAssistantConfigurationRequest {
|
||||||
|
option (id) = 121;
|
||||||
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (ifdef) = "USE_VOICE_ASSISTANT";
|
||||||
|
}
|
||||||
|
|
||||||
|
message VoiceAssistantConfigurationResponse {
|
||||||
|
option (id) = 122;
|
||||||
|
option (source) = SOURCE_SERVER;
|
||||||
|
option (ifdef) = "USE_VOICE_ASSISTANT";
|
||||||
|
|
||||||
|
repeated VoiceAssistantWakeWord available_wake_words = 1;
|
||||||
|
repeated uint32 active_wake_words = 2;
|
||||||
|
uint32 max_active_wake_words = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VoiceAssistantSetConfiguration {
|
||||||
|
option (id) = 123;
|
||||||
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (ifdef) = "USE_VOICE_ASSISTANT";
|
||||||
|
|
||||||
|
repeated uint32 active_wake_words = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== ALARM CONTROL PANEL ====================
|
// ==================== ALARM CONTROL PANEL ====================
|
||||||
enum AlarmControlPanelState {
|
enum AlarmControlPanelState {
|
||||||
ALARM_STATE_DISARMED = 0;
|
ALARM_STATE_DISARMED = 0;
|
||||||
|
|
|
@ -1032,6 +1032,7 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
|
||||||
media_format.sample_rate = supported_format.sample_rate;
|
media_format.sample_rate = supported_format.sample_rate;
|
||||||
media_format.num_channels = supported_format.num_channels;
|
media_format.num_channels = supported_format.num_channels;
|
||||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||||
|
media_format.sample_bytes = supported_format.sample_bytes;
|
||||||
msg.supported_formats.push_back(media_format);
|
msg.supported_formats.push_back(media_format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5149,6 +5149,10 @@ bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt va
|
||||||
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
|
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
this->sample_bytes = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -5168,6 +5172,7 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_uint32(2, this->sample_rate);
|
buffer.encode_uint32(2, this->sample_rate);
|
||||||
buffer.encode_uint32(3, this->num_channels);
|
buffer.encode_uint32(3, this->num_channels);
|
||||||
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
|
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
|
||||||
|
buffer.encode_uint32(5, this->sample_bytes);
|
||||||
}
|
}
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
|
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
|
||||||
|
@ -5190,6 +5195,11 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
|
||||||
out.append(" purpose: ");
|
out.append(" purpose: ");
|
||||||
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
|
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" sample_bytes: ");
|
||||||
|
sprintf(buffer, "%" PRIu32, this->sample_bytes);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -7114,6 +7124,149 @@ void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
bool VoiceAssistantWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1: {
|
||||||
|
this->id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 2: {
|
||||||
|
this->wake_word = value.as_string();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
this->trained_languages.push_back(value.as_string());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const {
|
||||||
|
buffer.encode_uint32(1, this->id);
|
||||||
|
buffer.encode_string(2, this->wake_word);
|
||||||
|
for (auto &it : this->trained_languages) {
|
||||||
|
buffer.encode_string(3, it, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void VoiceAssistantWakeWord::dump_to(std::string &out) const {
|
||||||
|
__attribute__((unused)) char buffer[64];
|
||||||
|
out.append("VoiceAssistantWakeWord {\n");
|
||||||
|
out.append(" id: ");
|
||||||
|
sprintf(buffer, "%" PRIu32, this->id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" wake_word: ");
|
||||||
|
out.append("'").append(this->wake_word).append("'");
|
||||||
|
out.append("\n");
|
||||||
|
|
||||||
|
for (const auto &it : this->trained_languages) {
|
||||||
|
out.append(" trained_languages: ");
|
||||||
|
out.append("'").append(it).append("'");
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
|
out.append("}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {}
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
|
||||||
|
out.append("VoiceAssistantConfigurationRequest {}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 2: {
|
||||||
|
this->active_wake_words.push_back(value.as_uint32());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
this->max_active_wake_words = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1: {
|
||||||
|
this->available_wake_words.push_back(value.as_message<VoiceAssistantWakeWord>());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
|
for (auto &it : this->available_wake_words) {
|
||||||
|
buffer.encode_message<VoiceAssistantWakeWord>(1, it, true);
|
||||||
|
}
|
||||||
|
for (auto &it : this->active_wake_words) {
|
||||||
|
buffer.encode_uint32(2, it, true);
|
||||||
|
}
|
||||||
|
buffer.encode_uint32(3, this->max_active_wake_words);
|
||||||
|
}
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
|
||||||
|
__attribute__((unused)) char buffer[64];
|
||||||
|
out.append("VoiceAssistantConfigurationResponse {\n");
|
||||||
|
for (const auto &it : this->available_wake_words) {
|
||||||
|
out.append(" available_wake_words: ");
|
||||||
|
it.dump_to(out);
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &it : this->active_wake_words) {
|
||||||
|
out.append(" active_wake_words: ");
|
||||||
|
sprintf(buffer, "%" PRIu32, it);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
out.append(" max_active_wake_words: ");
|
||||||
|
sprintf(buffer, "%" PRIu32, this->max_active_wake_words);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
|
out.append("}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
bool VoiceAssistantSetConfiguration::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 1: {
|
||||||
|
this->active_wake_words.push_back(value.as_uint32());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const {
|
||||||
|
for (auto &it : this->active_wake_words) {
|
||||||
|
buffer.encode_uint32(1, it, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void VoiceAssistantSetConfiguration::dump_to(std::string &out) const {
|
||||||
|
__attribute__((unused)) char buffer[64];
|
||||||
|
out.append("VoiceAssistantSetConfiguration {\n");
|
||||||
|
for (const auto &it : this->active_wake_words) {
|
||||||
|
out.append(" active_wake_words: ");
|
||||||
|
sprintf(buffer, "%" PRIu32, it);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
|
out.append("}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 6: {
|
case 6: {
|
||||||
|
|
|
@ -1277,6 +1277,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
|
||||||
uint32_t sample_rate{0};
|
uint32_t sample_rate{0};
|
||||||
uint32_t num_channels{0};
|
uint32_t num_channels{0};
|
||||||
enums::MediaPlayerFormatPurpose purpose{};
|
enums::MediaPlayerFormatPurpose purpose{};
|
||||||
|
uint32_t sample_bytes{0};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
|
@ -1848,6 +1849,54 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
|
||||||
protected:
|
protected:
|
||||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
};
|
};
|
||||||
|
class VoiceAssistantWakeWord : public ProtoMessage {
|
||||||
|
public:
|
||||||
|
uint32_t id{0};
|
||||||
|
std::string wake_word{};
|
||||||
|
std::vector<std::string> trained_languages{};
|
||||||
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void dump_to(std::string &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||||
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
|
};
|
||||||
|
class VoiceAssistantConfigurationRequest : public ProtoMessage {
|
||||||
|
public:
|
||||||
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void dump_to(std::string &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
};
|
||||||
|
class VoiceAssistantConfigurationResponse : public ProtoMessage {
|
||||||
|
public:
|
||||||
|
std::vector<VoiceAssistantWakeWord> available_wake_words{};
|
||||||
|
std::vector<uint32_t> active_wake_words{};
|
||||||
|
uint32_t max_active_wake_words{0};
|
||||||
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void dump_to(std::string &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||||
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
|
};
|
||||||
|
class VoiceAssistantSetConfiguration : public ProtoMessage {
|
||||||
|
public:
|
||||||
|
std::vector<uint32_t> active_wake_words{};
|
||||||
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
void dump_to(std::string &out) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
|
};
|
||||||
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
|
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
std::string object_id{};
|
std::string object_id{};
|
||||||
|
|
|
@ -496,6 +496,19 @@ bool APIServerConnectionBase::send_voice_assistant_announce_finished(const Voice
|
||||||
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
|
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
bool APIServerConnectionBase::send_voice_assistant_configuration_response(
|
||||||
|
const VoiceAssistantConfigurationResponse &msg) {
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
return this->send_message_<VoiceAssistantConfigurationResponse>(msg, 122);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
#endif
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
|
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
|
||||||
const ListEntitiesAlarmControlPanelResponse &msg) {
|
const ListEntitiesAlarmControlPanelResponse &msg) {
|
||||||
|
@ -1156,6 +1169,28 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||||
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
|
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
|
||||||
#endif
|
#endif
|
||||||
this->on_voice_assistant_announce_request(msg);
|
this->on_voice_assistant_announce_request(msg);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 121: {
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
VoiceAssistantConfigurationRequest msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
this->on_voice_assistant_configuration_request(msg);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 123: {
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
VoiceAssistantSetConfiguration msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
this->on_voice_assistant_set_configuration(msg);
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,6 +253,15 @@ class APIServerConnectionBase : public ProtoService {
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
|
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
|
||||||
|
#endif
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
|
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -37,6 +37,7 @@ struct MediaPlayerSupportedFormat {
|
||||||
uint32_t sample_rate;
|
uint32_t sample_rate;
|
||||||
uint32_t num_channels;
|
uint32_t num_channels;
|
||||||
MediaPlayerFormatPurpose purpose;
|
MediaPlayerFormatPurpose purpose;
|
||||||
|
uint32_t sample_bytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MediaPlayer;
|
class MediaPlayer;
|
||||||
|
|
|
@ -1,27 +1,29 @@
|
||||||
import binascii
|
import binascii
|
||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import modbus
|
from esphome.components import modbus
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_NAME,
|
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
|
CONF_NAME,
|
||||||
CONF_OFFSET,
|
CONF_OFFSET,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
)
|
)
|
||||||
from esphome.cpp_helpers import logging
|
from esphome.cpp_helpers import logging
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALLOW_DUPLICATE_COMMANDS,
|
CONF_ALLOW_DUPLICATE_COMMANDS,
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
CONF_BYTE_OFFSET,
|
CONF_BYTE_OFFSET,
|
||||||
CONF_COMMAND_THROTTLE,
|
CONF_COMMAND_THROTTLE,
|
||||||
CONF_OFFLINE_SKIP_UPDATES,
|
|
||||||
CONF_CUSTOM_COMMAND,
|
CONF_CUSTOM_COMMAND,
|
||||||
CONF_FORCE_NEW_RANGE,
|
CONF_FORCE_NEW_RANGE,
|
||||||
CONF_MODBUS_CONTROLLER_ID,
|
|
||||||
CONF_MAX_CMD_RETRIES,
|
CONF_MAX_CMD_RETRIES,
|
||||||
|
CONF_MODBUS_CONTROLLER_ID,
|
||||||
|
CONF_OFFLINE_SKIP_UPDATES,
|
||||||
CONF_ON_COMMAND_SENT,
|
CONF_ON_COMMAND_SENT,
|
||||||
CONF_REGISTER_COUNT,
|
CONF_REGISTER_COUNT,
|
||||||
CONF_REGISTER_TYPE,
|
CONF_REGISTER_TYPE,
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import binary_sensor
|
from esphome.components import binary_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
|
||||||
|
|
||||||
from esphome.const import CONF_ADDRESS, CONF_ID
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
MODBUS_REGISTER_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
MODBUS_REGISTER_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import number
|
from esphome.components import number
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
|
@ -12,14 +12,13 @@ from esphome.const import (
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
MODBUS_WRITE_REGISTER_TYPE,
|
MODBUS_WRITE_REGISTER_TYPE,
|
||||||
add_modbus_base_properties,
|
SENSOR_VALUE_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
SENSOR_VALUE_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
CONF_CUSTOM_COMMAND,
|
CONF_CUSTOM_COMMAND,
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import output
|
from esphome.components import output
|
||||||
from esphome.const import (
|
import esphome.config_validation as cv
|
||||||
CONF_ADDRESS,
|
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY
|
||||||
CONF_ID,
|
|
||||||
CONF_MULTIPLY,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
modbus_controller_ns,
|
SENSOR_VALUE_TYPE,
|
||||||
modbus_calc_properties,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
SENSOR_VALUE_TYPE,
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_MODBUS_CONTROLLER_ID,
|
CONF_MODBUS_CONTROLLER_ID,
|
||||||
CONF_REGISTER_TYPE,
|
CONF_REGISTER_TYPE,
|
||||||
|
@ -65,6 +60,7 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
byte_offset, reg_count = modbus_calc_properties(config)
|
byte_offset, reg_count = modbus_calc_properties(config)
|
||||||
# Binary Output
|
# Binary Output
|
||||||
|
write_template = None
|
||||||
if config[CONF_REGISTER_TYPE] == "coil":
|
if config[CONF_REGISTER_TYPE] == "coil":
|
||||||
var = cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
|
@ -72,7 +68,7 @@ async def to_code(config):
|
||||||
byte_offset,
|
byte_offset,
|
||||||
)
|
)
|
||||||
if CONF_WRITE_LAMBDA in config:
|
if CONF_WRITE_LAMBDA in config:
|
||||||
template_ = await cg.process_lambda(
|
write_template = await cg.process_lambda(
|
||||||
config[CONF_WRITE_LAMBDA],
|
config[CONF_WRITE_LAMBDA],
|
||||||
[
|
[
|
||||||
(ModbusBinaryOutput.operator("ptr"), "item"),
|
(ModbusBinaryOutput.operator("ptr"), "item"),
|
||||||
|
@ -92,7 +88,7 @@ async def to_code(config):
|
||||||
)
|
)
|
||||||
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
|
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
|
||||||
if CONF_WRITE_LAMBDA in config:
|
if CONF_WRITE_LAMBDA in config:
|
||||||
template_ = await cg.process_lambda(
|
write_template = await cg.process_lambda(
|
||||||
config[CONF_WRITE_LAMBDA],
|
config[CONF_WRITE_LAMBDA],
|
||||||
[
|
[
|
||||||
(ModbusFloatOutput.operator("ptr"), "item"),
|
(ModbusFloatOutput.operator("ptr"), "item"),
|
||||||
|
@ -105,5 +101,5 @@ async def to_code(config):
|
||||||
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
|
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
|
||||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||||
cg.add(var.set_parent(parent))
|
cg.add(var.set_parent(parent))
|
||||||
if CONF_WRITE_LAMBDA in config:
|
if write_template:
|
||||||
cg.add(var.set_write_template(template_))
|
cg.add(var.set_write_template(write_template))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import select
|
from esphome.components import select
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
|
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor
|
from esphome.components import sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
from esphome.const import CONF_ID, CONF_ADDRESS
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
|
||||||
SensorItem,
|
|
||||||
MODBUS_REGISTER_TYPE,
|
MODBUS_REGISTER_TYPE,
|
||||||
SENSOR_VALUE_TYPE,
|
SENSOR_VALUE_TYPE,
|
||||||
|
ModbusItemBaseSchema,
|
||||||
|
SensorItem,
|
||||||
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import switch
|
from esphome.components import switch
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
|
|
||||||
from esphome.const import CONF_ID, CONF_ADDRESS
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
MODBUS_REGISTER_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
MODBUS_REGISTER_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import text_sensor
|
from esphome.components import text_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
|
||||||
|
|
||||||
|
|
||||||
from esphome.const import CONF_ADDRESS, CONF_ID
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
MODBUS_REGISTER_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
MODBUS_REGISTER_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_FORCE_NEW_RANGE,
|
CONF_FORCE_NEW_RANGE,
|
||||||
CONF_MODBUS_CONTROLLER_ID,
|
CONF_MODBUS_CONTROLLER_ID,
|
||||||
|
CONF_RAW_ENCODE,
|
||||||
CONF_REGISTER_COUNT,
|
CONF_REGISTER_COUNT,
|
||||||
|
CONF_REGISTER_TYPE,
|
||||||
CONF_RESPONSE_SIZE,
|
CONF_RESPONSE_SIZE,
|
||||||
CONF_SKIP_UPDATES,
|
CONF_SKIP_UPDATES,
|
||||||
CONF_RAW_ENCODE,
|
|
||||||
CONF_REGISTER_TYPE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ["modbus_controller"]
|
DEPENDENCIES = ["modbus_controller"]
|
||||||
|
|
57
esphome/components/opentherm/__init__.py
Normal file
57
esphome/components/opentherm/__init__.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||||
|
|
||||||
|
CODEOWNERS = ["@olegtarasov"]
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
CONF_IN_PIN = "in_pin"
|
||||||
|
CONF_OUT_PIN = "out_pin"
|
||||||
|
CONF_CH_ENABLE = "ch_enable"
|
||||||
|
CONF_DHW_ENABLE = "dhw_enable"
|
||||||
|
CONF_COOLING_ENABLE = "cooling_enable"
|
||||||
|
CONF_OTC_ACTIVE = "otc_active"
|
||||||
|
CONF_CH2_ACTIVE = "ch2_active"
|
||||||
|
CONF_SYNC_MODE = "sync_mode"
|
||||||
|
|
||||||
|
opentherm_ns = cg.esphome_ns.namespace("opentherm")
|
||||||
|
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(OpenthermHub),
|
||||||
|
cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema,
|
||||||
|
cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_CH_ENABLE, True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DHW_ENABLE, True): cv.boolean,
|
||||||
|
cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean,
|
||||||
|
cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean,
|
||||||
|
cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean,
|
||||||
|
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config: dict[str, Any]) -> None:
|
||||||
|
# Create the hub, passing the two callbacks defined below
|
||||||
|
# Since the hub is used in the callbacks, we need to define it first
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
# Set pins
|
||||||
|
in_pin = await cg.gpio_pin_expression(config[CONF_IN_PIN])
|
||||||
|
cg.add(var.set_in_pin(in_pin))
|
||||||
|
|
||||||
|
out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
|
||||||
|
cg.add(var.set_out_pin(out_pin))
|
||||||
|
|
||||||
|
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
|
||||||
|
for key, value in config.items():
|
||||||
|
if key not in non_sensors:
|
||||||
|
cg.add(getattr(var, f"set_{key}")(value))
|
277
esphome/components/opentherm/hub.cpp
Normal file
277
esphome/components/opentherm/hub.cpp
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
#include "hub.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
static const char *const TAG = "opentherm";
|
||||||
|
|
||||||
|
OpenthermData OpenthermHub::build_request_(MessageId request_id) {
|
||||||
|
OpenthermData data;
|
||||||
|
data.type = 0;
|
||||||
|
data.id = 0;
|
||||||
|
data.valueHB = 0;
|
||||||
|
data.valueLB = 0;
|
||||||
|
|
||||||
|
// First, handle the status request. This requires special logic, because we
|
||||||
|
// wouldn't want to inadvertently disable domestic hot water, for example.
|
||||||
|
// It is also included in the macro-generated code below, but that will
|
||||||
|
// never be executed, because we short-circuit it here.
|
||||||
|
if (request_id == MessageId::STATUS) {
|
||||||
|
bool const ch_enabled = this->ch_enable;
|
||||||
|
bool dhw_enabled = this->dhw_enable;
|
||||||
|
bool cooling_enabled = this->cooling_enable;
|
||||||
|
bool otc_enabled = this->otc_active;
|
||||||
|
bool ch2_enabled = this->ch2_active;
|
||||||
|
|
||||||
|
data.type = MessageType::READ_DATA;
|
||||||
|
data.id = MessageId::STATUS;
|
||||||
|
data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4);
|
||||||
|
|
||||||
|
// Disable incomplete switch statement warnings, because the cases in each
|
||||||
|
// switch are generated based on the configured sensors and inputs.
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wswitch"
|
||||||
|
|
||||||
|
// TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on
|
||||||
|
// which sensors are enabled in config.
|
||||||
|
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return OpenthermData();
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenthermHub::OpenthermHub() : Component() {}
|
||||||
|
|
||||||
|
void OpenthermHub::process_response(OpenthermData &data) {
|
||||||
|
ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
|
||||||
|
this->opentherm_->message_id_to_str((MessageId) data.id));
|
||||||
|
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::setup() {
|
||||||
|
ESP_LOGD(TAG, "Setting up OpenTherm component");
|
||||||
|
this->opentherm_ = make_unique<OpenTherm>(this->in_pin_, this->out_pin_);
|
||||||
|
if (!this->opentherm_->initialize()) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details.");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there is at least one request, as we are required to
|
||||||
|
// communicate at least once every second. Sending the status request is
|
||||||
|
// good practice anyway.
|
||||||
|
this->add_repeating_message(MessageId::STATUS);
|
||||||
|
|
||||||
|
this->current_message_iterator_ = this->initial_messages_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); }
|
||||||
|
|
||||||
|
void OpenthermHub::loop() {
|
||||||
|
if (this->sync_mode_) {
|
||||||
|
this->sync_loop_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cur_time = millis();
|
||||||
|
auto const cur_mode = this->opentherm_->get_mode();
|
||||||
|
switch (cur_mode) {
|
||||||
|
case OperationMode::WRITE:
|
||||||
|
case OperationMode::READ:
|
||||||
|
case OperationMode::LISTEN:
|
||||||
|
if (!this->check_timings_(cur_time)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->last_mode_ = cur_mode;
|
||||||
|
break;
|
||||||
|
case OperationMode::ERROR_PROTOCOL:
|
||||||
|
if (this->last_mode_ == OperationMode::WRITE) {
|
||||||
|
this->handle_protocol_write_error_();
|
||||||
|
} else if (this->last_mode_ == OperationMode::READ) {
|
||||||
|
this->handle_protocol_read_error_();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->stop_opentherm_();
|
||||||
|
break;
|
||||||
|
case OperationMode::ERROR_TIMEOUT:
|
||||||
|
this->handle_timeout_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
break;
|
||||||
|
case OperationMode::IDLE:
|
||||||
|
if (this->should_skip_loop_(cur_time)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->start_conversation_();
|
||||||
|
break;
|
||||||
|
case OperationMode::SENT:
|
||||||
|
// Message sent, now listen for the response.
|
||||||
|
this->opentherm_->listen();
|
||||||
|
break;
|
||||||
|
case OperationMode::RECEIVED:
|
||||||
|
this->read_response_();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::sync_loop_() {
|
||||||
|
if (!this->opentherm_->is_idle()) {
|
||||||
|
ESP_LOGE(TAG, "OpenTherm is not idle at the start of the loop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cur_time = millis();
|
||||||
|
|
||||||
|
this->check_timings_(cur_time);
|
||||||
|
|
||||||
|
if (this->should_skip_loop_(cur_time)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->start_conversation_();
|
||||||
|
|
||||||
|
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
|
||||||
|
ESP_LOGE(TAG, "Hub timeout triggered during send");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->opentherm_->is_error()) {
|
||||||
|
this->handle_protocol_write_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
} else if (!this->opentherm_->is_sent()) {
|
||||||
|
ESP_LOGW(TAG, "Unexpected state after sending request: %s",
|
||||||
|
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for the response
|
||||||
|
this->opentherm_->listen();
|
||||||
|
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
|
||||||
|
ESP_LOGE(TAG, "Hub timeout triggered during receive");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->opentherm_->is_timeout()) {
|
||||||
|
this->handle_timeout_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
} else if (this->opentherm_->is_protocol_error()) {
|
||||||
|
this->handle_protocol_read_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
} else if (!this->opentherm_->has_message()) {
|
||||||
|
ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
|
||||||
|
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->read_response_();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenthermHub::check_timings_(uint32_t cur_time) {
|
||||||
|
if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other "
|
||||||
|
"components that might slow the loop down.",
|
||||||
|
(int) (cur_time - this->last_conversation_start_));
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
|
||||||
|
if (this->last_conversation_end_ > 0 && (cur_time - this->last_conversation_end_) < 100) {
|
||||||
|
ESP_LOGV(TAG, "Less than 100 ms elapsed since last convo, skipping this iteration");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::start_conversation_() {
|
||||||
|
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) {
|
||||||
|
this->sending_initial_ = false;
|
||||||
|
this->current_message_iterator_ = this->repeating_messages_.begin();
|
||||||
|
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) {
|
||||||
|
this->current_message_iterator_ = this->repeating_messages_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto request = this->build_request_(*this->current_message_iterator_);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
|
||||||
|
this->opentherm_->message_id_to_str((MessageId) request.id));
|
||||||
|
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str());
|
||||||
|
// Send the request
|
||||||
|
this->last_conversation_start_ = millis();
|
||||||
|
this->opentherm_->send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::read_response_() {
|
||||||
|
OpenthermData response;
|
||||||
|
if (!this->opentherm_->get_message(response)) {
|
||||||
|
ESP_LOGW(TAG, "Couldn't get the response, but flags indicated success. This is a bug.");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->stop_opentherm_();
|
||||||
|
|
||||||
|
this->process_response(response);
|
||||||
|
|
||||||
|
this->current_message_iterator_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::stop_opentherm_() {
|
||||||
|
this->opentherm_->stop();
|
||||||
|
this->last_conversation_end_ = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::handle_protocol_write_error_() {
|
||||||
|
ESP_LOGW(TAG, "Error while sending request: %s",
|
||||||
|
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||||
|
ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::handle_protocol_read_error_() {
|
||||||
|
OpenThermError error;
|
||||||
|
this->opentherm_->get_protocol_error(error);
|
||||||
|
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::handle_timeout_error_() {
|
||||||
|
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ID(x) x
|
||||||
|
#define SHOW2(x) #x
|
||||||
|
#define SHOW(x) SHOW2(x)
|
||||||
|
|
||||||
|
void OpenthermHub::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "OpenTherm:");
|
||||||
|
LOG_PIN(" In: ", this->in_pin_);
|
||||||
|
LOG_PIN(" Out: ", this->out_pin_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Initial requests:");
|
||||||
|
for (auto type : this->initial_messages_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " - %d", type);
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Repeating requests:");
|
||||||
|
for (auto type : this->repeating_messages_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " - %d", type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
110
esphome/components/opentherm/hub.h
Normal file
110
esphome/components/opentherm/hub.h
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include "opentherm.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
// OpenTherm component for ESPHome
|
||||||
|
class OpenthermHub : public Component {
|
||||||
|
protected:
|
||||||
|
// Communication pins for the OpenTherm interface
|
||||||
|
InternalGPIOPin *in_pin_, *out_pin_;
|
||||||
|
// The OpenTherm interface
|
||||||
|
std::unique_ptr<OpenTherm> opentherm_;
|
||||||
|
|
||||||
|
// The set of initial messages to send on starting communication with the boiler
|
||||||
|
std::unordered_set<MessageId> initial_messages_;
|
||||||
|
// and the repeating messages which are sent repeatedly to update various sensors
|
||||||
|
// and boiler parameters (like the setpoint).
|
||||||
|
std::unordered_set<MessageId> repeating_messages_;
|
||||||
|
// Indicates if we are still working on the initial requests or not
|
||||||
|
bool sending_initial_ = true;
|
||||||
|
// Index for the current request in one of the _requests sets.
|
||||||
|
std::unordered_set<MessageId>::const_iterator current_message_iterator_;
|
||||||
|
|
||||||
|
uint32_t last_conversation_start_ = 0;
|
||||||
|
uint32_t last_conversation_end_ = 0;
|
||||||
|
OperationMode last_mode_ = IDLE;
|
||||||
|
OpenthermData last_request_;
|
||||||
|
|
||||||
|
// Synchronous communication mode prevents other components from disabling interrupts while
|
||||||
|
// we are talking to the boiler. Enable if you experience random intermittent invalid response errors.
|
||||||
|
// Very likely to happen while using Dallas temperature sensors.
|
||||||
|
bool sync_mode_ = false;
|
||||||
|
|
||||||
|
// Create OpenTherm messages based on the message id
|
||||||
|
OpenthermData build_request_(MessageId request_id);
|
||||||
|
void handle_protocol_write_error_();
|
||||||
|
void handle_protocol_read_error_();
|
||||||
|
void handle_timeout_error_();
|
||||||
|
void stop_opentherm_();
|
||||||
|
void start_conversation_();
|
||||||
|
void read_response_();
|
||||||
|
bool check_timings_(uint32_t cur_time);
|
||||||
|
bool should_skip_loop_(uint32_t cur_time) const;
|
||||||
|
void sync_loop_();
|
||||||
|
|
||||||
|
template<typename F> bool spin_wait_(uint32_t timeout, F func) {
|
||||||
|
auto start_time = millis();
|
||||||
|
while (func()) {
|
||||||
|
yield();
|
||||||
|
auto cur_time = millis();
|
||||||
|
if (cur_time - start_time >= timeout) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor with references to the global interrupt handlers
|
||||||
|
OpenthermHub();
|
||||||
|
|
||||||
|
// Handle responses from the OpenTherm interface
|
||||||
|
void process_response(OpenthermData &data);
|
||||||
|
|
||||||
|
// Setters for the input and output OpenTherm interface pins
|
||||||
|
void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; }
|
||||||
|
void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; }
|
||||||
|
|
||||||
|
// Add a request to the set of initial requests
|
||||||
|
void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
|
||||||
|
// Add a request to the set of repeating requests. Note that a large number of repeating
|
||||||
|
// requests will slow down communication with the boiler. Each request may take up to 1 second,
|
||||||
|
// so with all sensors enabled, it may take about half a minute before a change in setpoint
|
||||||
|
// will be processed.
|
||||||
|
void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
|
||||||
|
|
||||||
|
// There are five status variables, which can either be set as a simple variable,
|
||||||
|
// or using a switch. ch_enable and dhw_enable default to true, the others to false.
|
||||||
|
bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false;
|
||||||
|
|
||||||
|
// Setters for the status variables
|
||||||
|
void set_ch_enable(bool value) { this->ch_enable = value; }
|
||||||
|
void set_dhw_enable(bool value) { this->dhw_enable = value; }
|
||||||
|
void set_cooling_enable(bool value) { this->cooling_enable = value; }
|
||||||
|
void set_otc_active(bool value) { this->otc_active = value; }
|
||||||
|
void set_ch2_active(bool value) { this->ch2_active = value; }
|
||||||
|
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
|
||||||
|
|
||||||
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void on_shutdown() override;
|
||||||
|
void loop() override;
|
||||||
|
void dump_config() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
568
esphome/components/opentherm/opentherm.cpp
Normal file
568
esphome/components/opentherm/opentherm.cpp
Normal file
|
@ -0,0 +1,568 @@
|
||||||
|
/*
|
||||||
|
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
|
||||||
|
* heavily modified to comply with ESPHome coding standards and provide better logging.
|
||||||
|
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "opentherm.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
#include "driver/timer.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#endif
|
||||||
|
#ifdef ESP8266
|
||||||
|
#include "Arduino.h"
|
||||||
|
#endif
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <bitset>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::bitset;
|
||||||
|
using std::stringstream;
|
||||||
|
using std::to_string;
|
||||||
|
|
||||||
|
static const char *const TAG = "opentherm";
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
OpenTherm *OpenTherm::instance_ = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout)
|
||||||
|
: in_pin_(in_pin),
|
||||||
|
out_pin_(out_pin),
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
timer_group_(TIMER_GROUP_0),
|
||||||
|
timer_idx_(TIMER_0),
|
||||||
|
#endif
|
||||||
|
mode_(OperationMode::IDLE),
|
||||||
|
error_type_(ProtocolErrorType::NO_ERROR),
|
||||||
|
capture_(0),
|
||||||
|
clock_(0),
|
||||||
|
data_(0),
|
||||||
|
bit_pos_(0),
|
||||||
|
timeout_counter_(-1),
|
||||||
|
device_timeout_(device_timeout) {
|
||||||
|
this->isr_in_pin_ = in_pin->to_isr();
|
||||||
|
this->isr_out_pin_ = out_pin->to_isr();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenTherm::initialize() {
|
||||||
|
#ifdef ESP8266
|
||||||
|
OpenTherm::instance_ = this;
|
||||||
|
#endif
|
||||||
|
this->in_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||||
|
this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||||
|
this->out_pin_->digital_write(true);
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
return this->init_esp32_timer_();
|
||||||
|
#else
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::listen() {
|
||||||
|
this->stop_timer_();
|
||||||
|
this->timeout_counter_ = this->device_timeout_ * 5; // timer_ ticks at 5 ticks/ms
|
||||||
|
|
||||||
|
this->mode_ = OperationMode::LISTEN;
|
||||||
|
this->data_ = 0;
|
||||||
|
this->bit_pos_ = 0;
|
||||||
|
|
||||||
|
this->start_read_timer_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::send(OpenthermData &data) {
|
||||||
|
this->stop_timer_();
|
||||||
|
this->data_ = data.type;
|
||||||
|
this->data_ = (this->data_ << 12) | data.id;
|
||||||
|
this->data_ = (this->data_ << 8) | data.valueHB;
|
||||||
|
this->data_ = (this->data_ << 8) | data.valueLB;
|
||||||
|
if (!check_parity_(this->data_)) {
|
||||||
|
this->data_ = this->data_ | 0x80000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->clock_ = 1; // clock starts at HIGH
|
||||||
|
this->bit_pos_ = 33; // count down (33 == start bit, 32-1 data, 0 == stop bit)
|
||||||
|
this->mode_ = OperationMode::WRITE;
|
||||||
|
|
||||||
|
this->start_write_timer_();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenTherm::get_message(OpenthermData &data) {
|
||||||
|
if (this->mode_ == OperationMode::RECEIVED) {
|
||||||
|
data.type = (this->data_ >> 28) & 0x7;
|
||||||
|
data.id = (this->data_ >> 16) & 0xFF;
|
||||||
|
data.valueHB = (this->data_ >> 8) & 0xFF;
|
||||||
|
data.valueLB = this->data_ & 0xFF;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenTherm::get_protocol_error(OpenThermError &error) {
|
||||||
|
if (this->mode_ != OperationMode::ERROR_PROTOCOL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.error_type = this->error_type_;
|
||||||
|
error.bit_pos = this->bit_pos_;
|
||||||
|
error.capture = this->capture_;
|
||||||
|
error.clock = this->clock_;
|
||||||
|
error.data = this->data_;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::stop() {
|
||||||
|
this->stop_timer_();
|
||||||
|
this->mode_ = OperationMode::IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::read_() {
|
||||||
|
this->data_ = 0;
|
||||||
|
this->bit_pos_ = 0;
|
||||||
|
this->mode_ = OperationMode::READ;
|
||||||
|
this->capture_ = 1; // reset counter and add as if read start bit
|
||||||
|
this->clock_ = 1; // clock is high at the start of comm
|
||||||
|
this->start_read_timer_(); // get us into 1/4 of manchester code. 5 timer ticks constitute 1 ms, which is 1 bit
|
||||||
|
// period in OpenTherm.
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
|
||||||
|
if (arg->mode_ == OperationMode::LISTEN) {
|
||||||
|
if (arg->timeout_counter_ == 0) {
|
||||||
|
arg->mode_ = OperationMode::ERROR_TIMEOUT;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool const value = arg->isr_in_pin_.digital_read();
|
||||||
|
if (value) { // incoming data (rising signal)
|
||||||
|
arg->read_();
|
||||||
|
}
|
||||||
|
if (arg->timeout_counter_ > 0) {
|
||||||
|
arg->timeout_counter_--;
|
||||||
|
}
|
||||||
|
} else if (arg->mode_ == OperationMode::READ) {
|
||||||
|
bool const value = arg->isr_in_pin_.digital_read();
|
||||||
|
uint8_t const last = (arg->capture_ & 1);
|
||||||
|
if (value != last) {
|
||||||
|
// transition of signal from last sampling
|
||||||
|
if (arg->clock_ == 1 && arg->capture_ > 0xF) {
|
||||||
|
// no transition in the middle of the bit
|
||||||
|
arg->mode_ = OperationMode::ERROR_PROTOCOL;
|
||||||
|
arg->error_type_ = ProtocolErrorType::NO_TRANSITION;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
} else if (arg->clock_ == 1 || arg->capture_ > 0xF) {
|
||||||
|
// transition in the middle of the bit OR no transition between two bit, both are valid data points
|
||||||
|
if (arg->bit_pos_ == BitPositions::STOP_BIT) {
|
||||||
|
// expecting stop bit
|
||||||
|
auto stop_bit_error = arg->verify_stop_bit_(last);
|
||||||
|
if (stop_bit_error == ProtocolErrorType::NO_ERROR) {
|
||||||
|
arg->mode_ = OperationMode::RECEIVED;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// end of data not verified, invalid data
|
||||||
|
arg->mode_ = OperationMode::ERROR_PROTOCOL;
|
||||||
|
arg->error_type_ = stop_bit_error;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// normal data point at clock high
|
||||||
|
arg->bit_read_(last);
|
||||||
|
arg->clock_ = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// clock low, not a data point, switch clock
|
||||||
|
arg->clock_ = 1;
|
||||||
|
}
|
||||||
|
arg->capture_ = 1; // reset counter
|
||||||
|
} else if (arg->capture_ > 0xFF) {
|
||||||
|
// no change for too long, invalid mancheter encoding
|
||||||
|
arg->mode_ = OperationMode::ERROR_PROTOCOL;
|
||||||
|
arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
arg->capture_ = (arg->capture_ << 1) | value;
|
||||||
|
} else if (arg->mode_ == OperationMode::WRITE) {
|
||||||
|
// write data to pin
|
||||||
|
if (arg->bit_pos_ == 33 || arg->bit_pos_ == 0) { // start bit
|
||||||
|
arg->write_bit_(1, arg->clock_);
|
||||||
|
} else { // data bits
|
||||||
|
arg->write_bit_(read_bit(arg->data_, arg->bit_pos_ - 1), arg->clock_);
|
||||||
|
}
|
||||||
|
if (arg->clock_ == 0) {
|
||||||
|
if (arg->bit_pos_ <= 0) { // check termination
|
||||||
|
arg->mode_ = OperationMode::SENT; // all data written
|
||||||
|
arg->stop_timer_();
|
||||||
|
}
|
||||||
|
arg->bit_pos_--;
|
||||||
|
arg->clock_ = 1;
|
||||||
|
} else {
|
||||||
|
arg->clock_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance_); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) {
|
||||||
|
this->data_ = (this->data_ << 1) | value;
|
||||||
|
this->bit_pos_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) {
|
||||||
|
if (value) { // stop bit detected
|
||||||
|
return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR;
|
||||||
|
} else { // no stop bit detected, error
|
||||||
|
return ProtocolErrorType::INVALID_STOP_BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) {
|
||||||
|
if (clock == 1) { // left part of manchester encoding
|
||||||
|
this->isr_out_pin_.digital_write(!high); // low means logical 1 to protocol
|
||||||
|
} else { // right part of manchester encoding
|
||||||
|
this->isr_out_pin_.digital_write(high); // high means logical 0 to protocol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
|
||||||
|
bool OpenTherm::init_esp32_timer_() {
|
||||||
|
// Search for a free timer. Maybe unstable, we'll see.
|
||||||
|
int cur_timer = 0;
|
||||||
|
timer_group_t timer_group = TIMER_GROUP_0;
|
||||||
|
timer_idx_t timer_idx = TIMER_0;
|
||||||
|
bool timer_found = false;
|
||||||
|
|
||||||
|
for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) {
|
||||||
|
timer_config_t temp_config;
|
||||||
|
timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1;
|
||||||
|
timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2);
|
||||||
|
|
||||||
|
auto err = timer_get_config(timer_group, timer_idx, &temp_config);
|
||||||
|
if (err == ESP_ERR_INVALID_ARG) {
|
||||||
|
// Error means timer was not initialized (or other things, but we are careful with our args)
|
||||||
|
timer_found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timer_found) {
|
||||||
|
ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx);
|
||||||
|
this->timer_group_ = timer_group;
|
||||||
|
this->timer_idx_ = timer_idx;
|
||||||
|
|
||||||
|
timer_config_t const config = {
|
||||||
|
.alarm_en = TIMER_ALARM_EN,
|
||||||
|
.counter_en = TIMER_PAUSE,
|
||||||
|
.intr_type = TIMER_INTR_LEVEL,
|
||||||
|
.counter_dir = TIMER_COUNT_UP,
|
||||||
|
.auto_reload = TIMER_AUTORELOAD_EN,
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
.clk_src = TIMER_SRC_CLK_DEFAULT,
|
||||||
|
#endif
|
||||||
|
.divider = 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t result;
|
||||||
|
|
||||||
|
result = timer_init(this->timer_group_, this->timer_idx_, &config);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to init timer. Error: %s", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast<bool (*)(void *)>(timer_isr),
|
||||||
|
this, 0);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) {
|
||||||
|
esp_err_t result;
|
||||||
|
|
||||||
|
result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_start(this->timer_group_, this->timer_idx_);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 kHz timer_
|
||||||
|
void IRAM_ATTR OpenTherm::start_read_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
this->start_esp32_timer_(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 kHz timer_
|
||||||
|
void IRAM_ATTR OpenTherm::start_write_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
this->start_esp32_timer_(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::stop_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
|
||||||
|
esp_err_t result;
|
||||||
|
|
||||||
|
result = timer_pause(this->timer_group_, this->timer_idx_);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // END ESP32
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
// 5 kHz timer_
|
||||||
|
void OpenTherm::start_read_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
|
||||||
|
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max)
|
||||||
|
timer1_write(1000); // 5kHz
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 kHz timer_
|
||||||
|
void OpenTherm::start_write_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
|
||||||
|
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max)
|
||||||
|
timer1_write(2500); // 2kHz
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::stop_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
timer1_disable();
|
||||||
|
timer1_detachInterrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // END ESP8266
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
|
||||||
|
bool OpenTherm::check_parity_(uint32_t val) {
|
||||||
|
val ^= val >> 16;
|
||||||
|
val ^= val >> 8;
|
||||||
|
val ^= val >> 4;
|
||||||
|
val ^= val >> 2;
|
||||||
|
val ^= val >> 1;
|
||||||
|
return (~val) & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TO_STRING_MEMBER(name) \
|
||||||
|
case name: \
|
||||||
|
return #name;
|
||||||
|
|
||||||
|
const char *OpenTherm::operation_mode_to_str(OperationMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
TO_STRING_MEMBER(IDLE)
|
||||||
|
TO_STRING_MEMBER(LISTEN)
|
||||||
|
TO_STRING_MEMBER(READ)
|
||||||
|
TO_STRING_MEMBER(RECEIVED)
|
||||||
|
TO_STRING_MEMBER(WRITE)
|
||||||
|
TO_STRING_MEMBER(SENT)
|
||||||
|
TO_STRING_MEMBER(ERROR_PROTOCOL)
|
||||||
|
TO_STRING_MEMBER(ERROR_TIMEOUT)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
|
||||||
|
switch (error_type) {
|
||||||
|
TO_STRING_MEMBER(NO_ERROR)
|
||||||
|
TO_STRING_MEMBER(NO_TRANSITION)
|
||||||
|
TO_STRING_MEMBER(INVALID_STOP_BIT)
|
||||||
|
TO_STRING_MEMBER(PARITY_ERROR)
|
||||||
|
TO_STRING_MEMBER(NO_CHANGE_TOO_LONG)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char *OpenTherm::message_type_to_str(MessageType message_type) {
|
||||||
|
switch (message_type) {
|
||||||
|
TO_STRING_MEMBER(READ_DATA)
|
||||||
|
TO_STRING_MEMBER(READ_ACK)
|
||||||
|
TO_STRING_MEMBER(WRITE_DATA)
|
||||||
|
TO_STRING_MEMBER(WRITE_ACK)
|
||||||
|
TO_STRING_MEMBER(INVALID_DATA)
|
||||||
|
TO_STRING_MEMBER(DATA_INVALID)
|
||||||
|
TO_STRING_MEMBER(UNKNOWN_DATAID)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *OpenTherm::message_id_to_str(MessageId id) {
|
||||||
|
switch (id) {
|
||||||
|
TO_STRING_MEMBER(STATUS)
|
||||||
|
TO_STRING_MEMBER(CH_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(CONTROLLER_CONFIG)
|
||||||
|
TO_STRING_MEMBER(DEVICE_CONFIG)
|
||||||
|
TO_STRING_MEMBER(COMMAND_CODE)
|
||||||
|
TO_STRING_MEMBER(FAULT_FLAGS)
|
||||||
|
TO_STRING_MEMBER(REMOTE)
|
||||||
|
TO_STRING_MEMBER(COOLING_CONTROL)
|
||||||
|
TO_STRING_MEMBER(CH2_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(CH_SETPOINT_OVERRIDE)
|
||||||
|
TO_STRING_MEMBER(TSP_COUNT)
|
||||||
|
TO_STRING_MEMBER(TSP_COMMAND)
|
||||||
|
TO_STRING_MEMBER(FHB_SIZE)
|
||||||
|
TO_STRING_MEMBER(FHB_COMMAND)
|
||||||
|
TO_STRING_MEMBER(MAX_MODULATION_LEVEL)
|
||||||
|
TO_STRING_MEMBER(MAX_BOILER_CAPACITY)
|
||||||
|
TO_STRING_MEMBER(ROOM_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(MODULATION_LEVEL)
|
||||||
|
TO_STRING_MEMBER(CH_WATER_PRESSURE)
|
||||||
|
TO_STRING_MEMBER(DHW_FLOW_RATE)
|
||||||
|
TO_STRING_MEMBER(DAY_TIME)
|
||||||
|
TO_STRING_MEMBER(DATE)
|
||||||
|
TO_STRING_MEMBER(YEAR)
|
||||||
|
TO_STRING_MEMBER(ROOM_SETPOINT_CH2)
|
||||||
|
TO_STRING_MEMBER(ROOM_TEMP)
|
||||||
|
TO_STRING_MEMBER(FEED_TEMP)
|
||||||
|
TO_STRING_MEMBER(DHW_TEMP)
|
||||||
|
TO_STRING_MEMBER(OUTSIDE_TEMP)
|
||||||
|
TO_STRING_MEMBER(RETURN_WATER_TEMP)
|
||||||
|
TO_STRING_MEMBER(SOLAR_STORE_TEMP)
|
||||||
|
TO_STRING_MEMBER(SOLAR_COLLECT_TEMP)
|
||||||
|
TO_STRING_MEMBER(FEED_TEMP_CH2)
|
||||||
|
TO_STRING_MEMBER(DHW2_TEMP)
|
||||||
|
TO_STRING_MEMBER(EXHAUST_TEMP)
|
||||||
|
TO_STRING_MEMBER(FAN_SPEED)
|
||||||
|
TO_STRING_MEMBER(FLAME_CURRENT)
|
||||||
|
TO_STRING_MEMBER(DHW_BOUNDS)
|
||||||
|
TO_STRING_MEMBER(CH_BOUNDS)
|
||||||
|
TO_STRING_MEMBER(OTC_CURVE_BOUNDS)
|
||||||
|
TO_STRING_MEMBER(DHW_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(MAX_CH_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(OTC_CURVE_RATIO)
|
||||||
|
TO_STRING_MEMBER(HVAC_STATUS)
|
||||||
|
TO_STRING_MEMBER(REL_VENT_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(DEVICE_VENT)
|
||||||
|
TO_STRING_MEMBER(REL_VENTILATION)
|
||||||
|
TO_STRING_MEMBER(REL_HUMID_EXHAUST)
|
||||||
|
TO_STRING_MEMBER(SUPPLY_INLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(EXHAUST_INLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(NOM_REL_VENTILATION)
|
||||||
|
TO_STRING_MEMBER(OVERRIDE_FUNC)
|
||||||
|
TO_STRING_MEMBER(OEM_DIAGNOSTIC)
|
||||||
|
TO_STRING_MEMBER(BURNER_STARTS)
|
||||||
|
TO_STRING_MEMBER(CH_PUMP_STARTS)
|
||||||
|
TO_STRING_MEMBER(DHW_PUMP_STARTS)
|
||||||
|
TO_STRING_MEMBER(DHW_BURNER_STARTS)
|
||||||
|
TO_STRING_MEMBER(BURNER_HOURS)
|
||||||
|
TO_STRING_MEMBER(CH_PUMP_HOURS)
|
||||||
|
TO_STRING_MEMBER(DHW_PUMP_HOURS)
|
||||||
|
TO_STRING_MEMBER(DHW_BURNER_HOURS)
|
||||||
|
TO_STRING_MEMBER(OT_VERSION_CONTROLLER)
|
||||||
|
TO_STRING_MEMBER(OT_VERSION_DEVICE)
|
||||||
|
TO_STRING_MEMBER(VERSION_CONTROLLER)
|
||||||
|
TO_STRING_MEMBER(VERSION_DEVICE)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string OpenTherm::debug_data(OpenthermData &data) {
|
||||||
|
stringstream result;
|
||||||
|
result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " "
|
||||||
|
<< bitset<8>(data.valueLB) << "\n";
|
||||||
|
result << "type: " << this->message_type_to_str((MessageType) data.type) << "; ";
|
||||||
|
result << "id: " << to_string(data.id) << "; ";
|
||||||
|
result << "HB: " << to_string(data.valueHB) << "; ";
|
||||||
|
result << "LB: " << to_string(data.valueLB) << "; ";
|
||||||
|
result << "uint_16: " << to_string(data.u16()) << "; ";
|
||||||
|
result << "float: " << to_string(data.f88());
|
||||||
|
|
||||||
|
return result.str();
|
||||||
|
}
|
||||||
|
std::string OpenTherm::debug_error(OpenThermError &error) {
|
||||||
|
stringstream result;
|
||||||
|
result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; ";
|
||||||
|
result << "data: ";
|
||||||
|
result << format_hex(error.data);
|
||||||
|
result << "; clock: " << to_string(clock_);
|
||||||
|
result << "; capture: " << bitset<32>(error.capture);
|
||||||
|
result << "; bit_pos: " << to_string(error.bit_pos);
|
||||||
|
|
||||||
|
return result.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
|
||||||
|
|
||||||
|
void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); }
|
||||||
|
|
||||||
|
uint16_t OpenthermData::u16() {
|
||||||
|
uint16_t const value = this->valueHB;
|
||||||
|
return (value << 8) | this->valueLB;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermData::u16(uint16_t value) {
|
||||||
|
this->valueLB = value & 0xFF;
|
||||||
|
this->valueHB = (value >> 8) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t OpenthermData::s16() {
|
||||||
|
int16_t const value = this->valueHB;
|
||||||
|
return (value << 8) | this->valueLB;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermData::s16(int16_t value) {
|
||||||
|
this->valueLB = value & 0xFF;
|
||||||
|
this->valueHB = (value >> 8) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
347
esphome/components/opentherm/opentherm.h
Normal file
347
esphome/components/opentherm/opentherm.h
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
/*
|
||||||
|
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
|
||||||
|
* heavily modified to comply with ESPHome coding standards and provide better logging.
|
||||||
|
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
#include "driver/timer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR
|
||||||
|
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
||||||
|
|
||||||
|
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
||||||
|
|
||||||
|
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
||||||
|
|
||||||
|
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
||||||
|
return bit_value ? setBit(value, bit) : clearBit(value, bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationMode {
|
||||||
|
IDLE = 0, // no operation
|
||||||
|
|
||||||
|
LISTEN = 1, // waiting for transmission to start
|
||||||
|
READ = 2, // reading 32-bit data frame
|
||||||
|
RECEIVED = 3, // data frame received with valid start and stop bit
|
||||||
|
|
||||||
|
WRITE = 4, // writing data with timer_
|
||||||
|
SENT = 5, // all data written to output
|
||||||
|
|
||||||
|
ERROR_PROTOCOL = 8, // manchester protocol data transfer error
|
||||||
|
ERROR_TIMEOUT = 9 // read timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ProtocolErrorType {
|
||||||
|
NO_ERROR = 0, // No error
|
||||||
|
NO_TRANSITION = 1, // No transition in the middle of the bit
|
||||||
|
INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected
|
||||||
|
PARITY_ERROR = 3, // Parity check didn't pass
|
||||||
|
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
|
||||||
|
};
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
READ_DATA = 0,
|
||||||
|
READ_ACK = 4,
|
||||||
|
WRITE_DATA = 1,
|
||||||
|
WRITE_ACK = 5,
|
||||||
|
INVALID_DATA = 2,
|
||||||
|
DATA_INVALID = 6,
|
||||||
|
UNKNOWN_DATAID = 7
|
||||||
|
};
|
||||||
|
|
||||||
|
enum MessageId {
|
||||||
|
STATUS = 0,
|
||||||
|
CH_SETPOINT = 1,
|
||||||
|
CONTROLLER_CONFIG = 2,
|
||||||
|
DEVICE_CONFIG = 3,
|
||||||
|
COMMAND_CODE = 4,
|
||||||
|
FAULT_FLAGS = 5,
|
||||||
|
REMOTE = 6,
|
||||||
|
COOLING_CONTROL = 7,
|
||||||
|
CH2_SETPOINT = 8,
|
||||||
|
CH_SETPOINT_OVERRIDE = 9,
|
||||||
|
TSP_COUNT = 10,
|
||||||
|
TSP_COMMAND = 11,
|
||||||
|
FHB_SIZE = 12,
|
||||||
|
FHB_COMMAND = 13,
|
||||||
|
MAX_MODULATION_LEVEL = 14,
|
||||||
|
MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level
|
||||||
|
ROOM_SETPOINT = 16,
|
||||||
|
MODULATION_LEVEL = 17,
|
||||||
|
CH_WATER_PRESSURE = 18,
|
||||||
|
DHW_FLOW_RATE = 19,
|
||||||
|
DAY_TIME = 20,
|
||||||
|
DATE = 21,
|
||||||
|
YEAR = 22,
|
||||||
|
ROOM_SETPOINT_CH2 = 23,
|
||||||
|
ROOM_TEMP = 24,
|
||||||
|
FEED_TEMP = 25,
|
||||||
|
DHW_TEMP = 26,
|
||||||
|
OUTSIDE_TEMP = 27,
|
||||||
|
RETURN_WATER_TEMP = 28,
|
||||||
|
SOLAR_STORE_TEMP = 29,
|
||||||
|
SOLAR_COLLECT_TEMP = 30,
|
||||||
|
FEED_TEMP_CH2 = 31,
|
||||||
|
DHW2_TEMP = 32,
|
||||||
|
EXHAUST_TEMP = 33,
|
||||||
|
FAN_SPEED = 35,
|
||||||
|
FLAME_CURRENT = 36,
|
||||||
|
DHW_BOUNDS = 48,
|
||||||
|
CH_BOUNDS = 49,
|
||||||
|
OTC_CURVE_BOUNDS = 50,
|
||||||
|
DHW_SETPOINT = 56,
|
||||||
|
MAX_CH_SETPOINT = 57,
|
||||||
|
OTC_CURVE_RATIO = 58,
|
||||||
|
|
||||||
|
// HVAC Specific Message IDs
|
||||||
|
HVAC_STATUS = 70,
|
||||||
|
REL_VENT_SETPOINT = 71,
|
||||||
|
DEVICE_VENT = 74,
|
||||||
|
REL_VENTILATION = 77,
|
||||||
|
REL_HUMID_EXHAUST = 78,
|
||||||
|
SUPPLY_INLET_TEMP = 80,
|
||||||
|
SUPPLY_OUTLET_TEMP = 81,
|
||||||
|
EXHAUST_INLET_TEMP = 82,
|
||||||
|
EXHAUST_OUTLET_TEMP = 83,
|
||||||
|
NOM_REL_VENTILATION = 87,
|
||||||
|
|
||||||
|
OVERRIDE_FUNC = 100,
|
||||||
|
OEM_DIAGNOSTIC = 115,
|
||||||
|
BURNER_STARTS = 116,
|
||||||
|
CH_PUMP_STARTS = 117,
|
||||||
|
DHW_PUMP_STARTS = 118,
|
||||||
|
DHW_BURNER_STARTS = 119,
|
||||||
|
BURNER_HOURS = 120,
|
||||||
|
CH_PUMP_HOURS = 121,
|
||||||
|
DHW_PUMP_HOURS = 122,
|
||||||
|
DHW_BURNER_HOURS = 123,
|
||||||
|
OT_VERSION_CONTROLLER = 124,
|
||||||
|
OT_VERSION_DEVICE = 125,
|
||||||
|
VERSION_CONTROLLER = 126,
|
||||||
|
VERSION_DEVICE = 127
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BitPositions { STOP_BIT = 33 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure to hold Opentherm data packet content.
|
||||||
|
* Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message.
|
||||||
|
*/
|
||||||
|
struct OpenthermData {
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t id;
|
||||||
|
uint8_t valueHB;
|
||||||
|
uint8_t valueLB;
|
||||||
|
|
||||||
|
OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return float representation of data packet value
|
||||||
|
*/
|
||||||
|
float f88();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param float number to set as value of this data packet
|
||||||
|
*/
|
||||||
|
void f88(float value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return unsigned 16b integer representation of data packet value
|
||||||
|
*/
|
||||||
|
uint16_t u16();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param unsigned 16b integer number to set as value of this data packet
|
||||||
|
*/
|
||||||
|
void u16(uint16_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return signed 16b integer representation of data packet value
|
||||||
|
*/
|
||||||
|
int16_t s16();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param signed 16b integer number to set as value of this data packet
|
||||||
|
*/
|
||||||
|
void s16(int16_t value);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OpenThermError {
|
||||||
|
ProtocolErrorType error_type;
|
||||||
|
uint32_t capture;
|
||||||
|
uint8_t clock;
|
||||||
|
uint32_t data;
|
||||||
|
uint8_t bit_pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opentherm static class that supports either listening or sending Opentherm data packets in the same time
|
||||||
|
*/
|
||||||
|
class OpenTherm {
|
||||||
|
public:
|
||||||
|
OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup pins.
|
||||||
|
*/
|
||||||
|
bool initialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening for Opentherm data packet comming from line connected to given pin.
|
||||||
|
* If data packet is received then has_message() function returns true and data packet can be retrieved by calling
|
||||||
|
* get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and
|
||||||
|
* if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received
|
||||||
|
* or timeout is reached listening is stopped.
|
||||||
|
*/
|
||||||
|
void listen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to check whether listen() function already captured a valid data packet.
|
||||||
|
*
|
||||||
|
* @return true if data packet has been captured from line by listen() function.
|
||||||
|
*/
|
||||||
|
bool has_message() { return mode_ == OperationMode::RECEIVED; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function
|
||||||
|
* returns true. This function can be called multiple times until stop() is called.
|
||||||
|
*
|
||||||
|
* @param data reference to data structure to which fill the data packet data.
|
||||||
|
* @return true if packet was ready and was filled into data structure passed, false otherwise.
|
||||||
|
*/
|
||||||
|
bool get_message(OpenthermData &data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately send out Opentherm data packet to line connected on given pin.
|
||||||
|
* Completed data transfer is indicated by is_sent() function.
|
||||||
|
* Error state is indicated by is_error() function.
|
||||||
|
*
|
||||||
|
* @param data Opentherm data packet.
|
||||||
|
*/
|
||||||
|
void send(OpenthermData &data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops listening for data packet or sending out data packet and resets internal state of this class.
|
||||||
|
* Stops all timers and unattaches all interrupts.
|
||||||
|
*/
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get protocol error details in case a protocol error occured.
|
||||||
|
* @param error reference to data structure to which fill the error details
|
||||||
|
* @return true if protocol error occured during last conversation, false otherwise.
|
||||||
|
*/
|
||||||
|
bool get_protocol_error(OpenThermError &error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to check whether send() function already finished sending data packed to line.
|
||||||
|
*
|
||||||
|
* @return true if data packet has been sent, false otherwise.
|
||||||
|
*/
|
||||||
|
bool is_sent() { return mode_ == OperationMode::SENT; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether listinig or sending is not in progress.
|
||||||
|
* That also means that no timers are running and no interrupts are attached.
|
||||||
|
*
|
||||||
|
* @return true if listening nor sending is in progress.
|
||||||
|
*/
|
||||||
|
bool is_idle() { return mode_ == OperationMode::IDLE; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and
|
||||||
|
* protocol errors.
|
||||||
|
*
|
||||||
|
* @return true if last listen() or send() operation ends up with an error.
|
||||||
|
*/
|
||||||
|
bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether last listen() or send() operation ends up with a *timeout* error
|
||||||
|
* @return true if last listen() or send() operation ends up with a *timeout* error.
|
||||||
|
*/
|
||||||
|
bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether last listen() or send() operation ends up with a *protocol* error
|
||||||
|
* @return true if last listen() or send() operation ends up with a *protocol* error.
|
||||||
|
*/
|
||||||
|
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
|
||||||
|
|
||||||
|
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
|
||||||
|
|
||||||
|
OperationMode get_mode() { return mode_; }
|
||||||
|
|
||||||
|
std::string debug_data(OpenthermData &data);
|
||||||
|
std::string debug_error(OpenThermError &error);
|
||||||
|
|
||||||
|
const char *protocol_error_to_to_str(ProtocolErrorType error_type);
|
||||||
|
const char *message_type_to_str(MessageType message_type);
|
||||||
|
const char *operation_mode_to_str(OperationMode mode);
|
||||||
|
const char *message_id_to_str(MessageId id);
|
||||||
|
|
||||||
|
static bool timer_isr(OpenTherm *arg);
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
static void esp8266_timer_isr();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
InternalGPIOPin *in_pin_;
|
||||||
|
InternalGPIOPin *out_pin_;
|
||||||
|
ISRInternalGPIOPin isr_in_pin_;
|
||||||
|
ISRInternalGPIOPin isr_out_pin_;
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
timer_group_t timer_group_;
|
||||||
|
timer_idx_t timer_idx_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
OperationMode mode_;
|
||||||
|
ProtocolErrorType error_type_;
|
||||||
|
uint32_t capture_;
|
||||||
|
uint8_t clock_;
|
||||||
|
uint32_t data_;
|
||||||
|
uint8_t bit_pos_;
|
||||||
|
int32_t timeout_counter_; // <0 no timeout
|
||||||
|
|
||||||
|
int32_t device_timeout_;
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
bool init_esp32_timer_();
|
||||||
|
void start_esp32_timer_(uint64_t alarm_value);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void stop_timer_();
|
||||||
|
|
||||||
|
void read_(); // data detected start reading
|
||||||
|
void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz)
|
||||||
|
void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz)
|
||||||
|
bool check_parity_(uint32_t val);
|
||||||
|
|
||||||
|
void bit_read_(uint8_t value);
|
||||||
|
ProtocolErrorType verify_stop_bit_(uint8_t value);
|
||||||
|
void write_bit_(uint8_t high, uint8_t clock);
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
// ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
|
||||||
|
static OpenTherm *instance_;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
|
@ -8,8 +8,22 @@ namespace st7701s {
|
||||||
void ST7701S::setup() {
|
void ST7701S::setup() {
|
||||||
esph_log_config(TAG, "Setting up ST7701S");
|
esph_log_config(TAG, "Setting up ST7701S");
|
||||||
this->spi_setup();
|
this->spi_setup();
|
||||||
|
this->write_init_sequence_();
|
||||||
|
}
|
||||||
|
|
||||||
|
// called after a delay after writing the init sequence
|
||||||
|
void ST7701S::complete_setup_() {
|
||||||
|
this->write_command_(SLEEP_OUT);
|
||||||
|
this->write_command_(DISPLAY_ON);
|
||||||
|
this->spi_teardown(); // SPI not needed after this
|
||||||
|
delay(10);
|
||||||
|
|
||||||
esp_lcd_rgb_panel_config_t config{};
|
esp_lcd_rgb_panel_config_t config{};
|
||||||
config.flags.fb_in_psram = 1;
|
config.flags.fb_in_psram = 1;
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
config.bounce_buffer_size_px = this->width_ * 10;
|
||||||
|
config.num_fbs = 1;
|
||||||
|
#endif // ESP_IDF_VERSION_MAJOR
|
||||||
config.timings.h_res = this->width_;
|
config.timings.h_res = this->width_;
|
||||||
config.timings.v_res = this->height_;
|
config.timings.v_res = this->height_;
|
||||||
config.timings.hsync_pulse_width = this->hsync_pulse_width_;
|
config.timings.hsync_pulse_width = this->hsync_pulse_width_;
|
||||||
|
@ -21,7 +35,6 @@ void ST7701S::setup() {
|
||||||
config.timings.flags.pclk_active_neg = this->pclk_inverted_;
|
config.timings.flags.pclk_active_neg = this->pclk_inverted_;
|
||||||
config.timings.pclk_hz = this->pclk_frequency_;
|
config.timings.pclk_hz = this->pclk_frequency_;
|
||||||
config.clk_src = LCD_CLK_SRC_PLL160M;
|
config.clk_src = LCD_CLK_SRC_PLL160M;
|
||||||
config.sram_trans_align = 64;
|
|
||||||
config.psram_trans_align = 64;
|
config.psram_trans_align = 64;
|
||||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||||
for (size_t i = 0; i != data_pin_count; i++) {
|
for (size_t i = 0; i != data_pin_count; i++) {
|
||||||
|
@ -34,15 +47,21 @@ void ST7701S::setup() {
|
||||||
config.de_gpio_num = this->de_pin_->get_pin();
|
config.de_gpio_num = this->de_pin_->get_pin();
|
||||||
config.pclk_gpio_num = this->pclk_pin_->get_pin();
|
config.pclk_gpio_num = this->pclk_pin_->get_pin();
|
||||||
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
|
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
|
||||||
|
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
|
||||||
|
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
|
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
|
|
||||||
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
|
|
||||||
this->write_init_sequence_();
|
|
||||||
esph_log_config(TAG, "ST7701S setup complete");
|
esph_log_config(TAG, "ST7701S setup complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ST7701S::loop() {
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
if (this->handle_ != nullptr)
|
||||||
|
esp_lcd_rgb_panel_restart(this->handle_);
|
||||||
|
#endif // ESP_IDF_VERSION_MAJOR
|
||||||
|
}
|
||||||
|
|
||||||
void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||||
if (w <= 0 || h <= 0)
|
if (w <= 0 || h <= 0)
|
||||||
|
@ -160,10 +179,7 @@ void ST7701S::write_init_sequence_() {
|
||||||
this->write_data_(val);
|
this->write_data_(val);
|
||||||
ESP_LOGD(TAG, "write MADCTL %X", val);
|
ESP_LOGD(TAG, "write MADCTL %X", val);
|
||||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||||
this->set_timeout(120, [this] {
|
this->set_timeout(120, [this] { this->complete_setup_(); });
|
||||||
this->write_command_(SLEEP_OUT);
|
|
||||||
this->write_command_(DISPLAY_ON);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ST7701S::dump_config() {
|
void ST7701S::dump_config() {
|
||||||
|
|
|
@ -33,6 +33,8 @@ class ST7701S : public display::Display,
|
||||||
public:
|
public:
|
||||||
void update() override { this->do_update_(); }
|
void update() override { this->do_update_(); }
|
||||||
void setup() override;
|
void setup() override;
|
||||||
|
void complete_setup_();
|
||||||
|
void loop() override;
|
||||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||||
|
|
||||||
|
|
|
@ -100,9 +100,6 @@ def valid_include(value):
|
||||||
def valid_project_name(value: str):
|
def valid_project_name(value: str):
|
||||||
if value.count(".") != 1:
|
if value.count(".") != 1:
|
||||||
raise cv.Invalid("project name needs to have a namespace")
|
raise cv.Invalid("project name needs to have a namespace")
|
||||||
|
|
||||||
value = value.replace(" ", "_")
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
pylint==3.1.0
|
pylint==3.2.7
|
||||||
flake8==7.0.0 # also change in .pre-commit-config.yaml when updating
|
flake8==7.0.0 # also change in .pre-commit-config.yaml when updating
|
||||||
black==24.4.2 # also change in .pre-commit-config.yaml when updating
|
black==24.4.2 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating
|
||||||
|
|
3
tests/components/opentherm/common.yaml
Normal file
3
tests/components/opentherm/common.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
opentherm:
|
||||||
|
in_pin: 1
|
||||||
|
out_pin: 2
|
1
tests/components/opentherm/test.esp32-ard.yaml
Normal file
1
tests/components/opentherm/test.esp32-ard.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp32-c3-ard.yaml
Normal file
1
tests/components/opentherm/test.esp32-c3-ard.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp32-c3-idf.yaml
Normal file
1
tests/components/opentherm/test.esp32-c3-idf.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp32-idf.yaml
Normal file
1
tests/components/opentherm/test.esp32-idf.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp8266-ard.yaml
Normal file
1
tests/components/opentherm/test.esp8266-ard.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
Loading…
Reference in a new issue