Merge branch 'dev' into nrf52_core

This commit is contained in:
tomaszduda23 2024-09-18 17:04:40 +02:00 committed by GitHub
commit db1f4b6f0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
118 changed files with 2654 additions and 431 deletions

View file

@ -47,6 +47,9 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.7.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: .
file: ./docker/Dockerfile
@ -70,6 +73,9 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.7.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: .
file: ./docker/Dockerfile

91
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,91 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
workflow_dispatch:
schedule:
- cron: "30 18 * * 4"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
# - language: c-cpp
# build-mode: autobuild
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View file

@ -36,7 +36,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v7.0.0
uses: peter-evans/create-pull-request@v7.0.3
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View file

@ -290,6 +290,7 @@ esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931

View file

@ -33,7 +33,7 @@ RUN \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
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 \
openssh-client=1:9.2p1-2+deb12u3 \
python3-cffi=1.15.1-5 \
@ -49,7 +49,7 @@ RUN \
zlib1g-dev=1:1.2.13.dfsg-1 \
libjpeg-dev=1:2.1.5-2 \
libfreetype-dev=2.12.1+dfsg-5+deb12u3 \
libssl-dev=3.0.14-1~deb12u1 \
libssl-dev=3.0.14-1~deb12u2 \
libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6+deb12u1 \
@ -96,14 +96,19 @@ RUN \
# First install requirements to leverage caching when requirements don't change
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini /
COPY requirements.txt requirements_optional.txt /
RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
&& pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
&& rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
&& export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \
pip3 install \
--break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
&& /platformio_install_deps.py /platformio.ini --libraries
--break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries
# Avoid unsafe git error when container user and file config volume permissions don't match
RUN git config --system --add safe.directory '*'

View file

@ -1,26 +1,26 @@
import logging
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import font
import esphome.components.image as espImage
from esphome.components.image import (
CONF_USE_TRANSPARENCY,
LOCAL_SCHEMA,
WEB_SCHEMA,
SOURCE_WEB,
SOURCE_LOCAL,
SOURCE_WEB,
WEB_SCHEMA,
)
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_REPEAT,
CONF_RESIZE,
CONF_TYPE,
CONF_SOURCE,
CONF_PATH,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
@ -172,6 +172,9 @@ async def to_code(config):
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = espImage.compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
try:
image = Image.open(path)
except Exception as e:
@ -183,8 +186,7 @@ async def to_code(config):
new_width_max, new_height_max = config[CONF_RESIZE]
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
else:
if width > 500 or height > 500:
elif width > 500 or height > 500:
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
@ -306,6 +308,8 @@ async def to_code(config):
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
else:
has_alpha = False
frame = image.convert("1", dither=Image.Dither.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])

View file

@ -62,6 +62,8 @@ service APIConnection {
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {}
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
}
@ -1118,6 +1120,7 @@ message MediaPlayerSupportedFormat {
uint32 sample_rate = 2;
uint32 num_channels = 3;
MediaPlayerFormatPurpose purpose = 4;
uint32 sample_bytes = 5;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
@ -1570,6 +1573,36 @@ message VoiceAssistantAnnounceFinished {
bool success = 1;
}
message VoiceAssistantWakeWord {
string 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 string 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 string active_wake_words = 1;
}
// ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0;

View file

@ -1033,6 +1033,7 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
media_format.sample_rate = supported_format.sample_rate;
media_format.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes;
msg.supported_formats.push_back(media_format);
}
@ -1224,6 +1225,39 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
}
}
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return resp;
}
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
resp.max_active_wake_words = config.max_active_wake_words;
}
return resp;
}
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View file

@ -153,6 +153,9 @@ class APIConnection : public APIServerConnection {
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View file

@ -5149,6 +5149,10 @@ bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt va
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
return true;
}
case 5: {
this->sample_bytes = value.as_uint32();
return true;
}
default:
return false;
}
@ -5168,6 +5172,7 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->sample_rate);
buffer.encode_uint32(3, this->num_channels);
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
buffer.encode_uint32(5, this->sample_bytes);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
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(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
out.append("\n");
out.append(" sample_bytes: ");
sprintf(buffer, "%" PRIu32, this->sample_bytes);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -7114,6 +7124,140 @@ void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->id = value.as_string();
return true;
}
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_string(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: ");
out.append("'").append(this->id).append("'");
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 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;
}
case 2: {
this->active_wake_words.push_back(value.as_string());
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_string(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: ");
out.append("'").append(it).append("'");
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_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->active_wake_words.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->active_wake_words) {
buffer.encode_string(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: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
#endif
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {

View file

@ -1277,6 +1277,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1848,6 +1849,53 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantWakeWord : public ProtoMessage {
public:
std::string id{};
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;
};
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<std::string> 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<std::string> 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_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
public:
std::string object_id{};

View file

@ -496,6 +496,19 @@ bool APIServerConnectionBase::send_voice_assistant_announce_finished(const Voice
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
}
#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
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
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());
#endif
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
break;
}
@ -1646,6 +1681,35 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
this->subscribe_voice_assistant(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
if (!this->send_voice_assistant_configuration_response(ret)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->voice_assistant_set_configuration(msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
if (!this->is_connection_setup()) {

View file

@ -253,6 +253,15 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_VOICE_ASSISTANT
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
#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
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
#endif
@ -425,6 +434,13 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
#endif
@ -526,6 +542,12 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
#endif

View file

@ -1,13 +1,13 @@
# Dummy integration to allow relying on AsyncTCP
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CORE, coroutine_with_priority
from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@OttoWinter"]
@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "2.1.3")
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4")
elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")

View file

@ -137,6 +137,7 @@ void BL0942::setup() {
}
this->write_reg_(BL0942_REG_USR_WRPROT, BL0942_REG_USR_WRPROT_MAGIC);
if (this->reset_)
this->write_reg_(BL0942_REG_SOFT_RESET, BL0942_REG_SOFT_RESET_MAGIC);
uint32_t mode = BL0942_REG_MODE_DEFAULT;
@ -196,6 +197,7 @@ void BL0942::received_package_(DataPacket *data) {
void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity)
ESP_LOGCONFIG(TAG, "BL0942:");
ESP_LOGCONFIG(TAG, " Reset: %s", TRUEFALSE(this->reset_));
ESP_LOGCONFIG(TAG, " Address: %d", this->address_);
ESP_LOGCONFIG(TAG, " Nominal line frequency: %d Hz", this->line_freq_);
ESP_LOGCONFIG(TAG, " Current reference: %f", this->current_reference_);

View file

@ -93,6 +93,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
void set_address(uint8_t address) { this->address_ = address; }
void set_reset(bool reset) { this->reset_ = reset; }
void set_current_reference(float current_ref) {
this->current_reference_ = current_ref;
this->current_reference_set_ = true;
@ -137,6 +138,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
float energy_reference_ = BL0942_EREF;
bool energy_reference_set_ = false;
uint8_t address_ = 0;
bool reset_ = false;
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
uint32_t rx_start_ = 0;
uint32_t prev_cf_cnt_ = 0;

View file

@ -27,6 +27,7 @@ from esphome.const import (
CONF_CURRENT_REFERENCE = "current_reference"
CONF_ENERGY_REFERENCE = "energy_reference"
CONF_POWER_REFERENCE = "power_reference"
CONF_RESET = "reset"
CONF_VOLTAGE_REFERENCE = "voltage_reference"
DEPENDENCIES = ["uart"]
@ -58,19 +59,19 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=0,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
accuracy_decimals=0,
accuracy_decimals=2,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
@ -82,6 +83,7 @@ CONFIG_SCHEMA = (
),
),
cv.Optional(CONF_ADDRESS, default=0): cv.int_range(min=0, max=3),
cv.Optional(CONF_RESET, default=True): cv.boolean,
cv.Optional(CONF_CURRENT_REFERENCE): cv.float_,
cv.Optional(CONF_ENERGY_REFERENCE): cv.float_,
cv.Optional(CONF_POWER_REFERENCE): cv.float_,
@ -115,6 +117,7 @@ async def to_code(config):
cg.add(var.set_frequency_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_reset(config[CONF_RESET]))
if (current_reference := config.get(CONF_CURRENT_REFERENCE, None)) is not None:
cg.add(var.set_current_reference(current_reference))
if (voltage_reference := config.get(CONF_VOLTAGE_REFERENCE, None)) is not None:

View file

@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_UUID): cv.uuid,
cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_TIMEOUT, default="5min"): cv.positive_time_period,
cv.Optional(CONF_MIN_RSSI): cv.All(
cv.decibel, cv.int_range(min=-100, max=-30)
@ -83,7 +83,7 @@ async def to_code(config):
cg.add(var.set_service_uuid128(uuid128))
if ibeacon_uuid := config.get(CONF_IBEACON_UUID):
ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(ibeacon_uuid))
ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid)
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:

View file

@ -256,6 +256,7 @@ bool Dsmr::parse_telegram() {
MyData data;
ESP_LOGV(TAG, "Trying to parse telegram");
this->stop_requesting_data_();
::dsmr::ParseResult<void> res =
::dsmr::P1Parser::parse(&data, this->telegram_, this->bytes_read_, false,
this->crc_check_); // Parse telegram according to data definition. Ignore unknown values.
@ -267,6 +268,11 @@ bool Dsmr::parse_telegram() {
} else {
this->status_clear_warning();
this->publish_sensors(data);
// publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(std::string(this->telegram_, this->bytes_read_));
}
return true;
}
}

View file

@ -85,6 +85,9 @@ class Dsmr : public Component, public uart::UARTDevice {
void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; }
DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, )
// handled outside dsmr
void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; }
protected:
void receive_telegram_();
void receive_encrypted_telegram_();
@ -124,6 +127,9 @@ class Dsmr : public Component, public uart::UARTDevice {
bool header_found_{false};
bool footer_found_{false};
// handled outside dsmr
text_sensor::TextSensor *s_telegram_{nullptr};
// Sensor member pointers
#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr};
DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, )

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import CONF_INTERNAL
from . import Dsmr, CONF_DSMR_ID
AUTO_LOAD = ["dsmr"]
@ -22,6 +22,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(),
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(),
cv.Optional("telegram"): text_sensor.text_sensor_schema().extend(
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -37,6 +40,8 @@ async def to_code(config):
if id and id.type == text_sensor.TextSensor:
var = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}")(var))
if key != "telegram":
# telegram is not handled by dsmr
text_sensors.append(f"F({key})")
if text_sensors:

View file

@ -239,7 +239,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 7)
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 8)
# The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32

View file

@ -31,6 +31,13 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128);
return ret;
}
ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
ESPBTUUID ret;
ret.uuid_.len = ESP_UUID_LEN_128;
for (int i = 0; i < ESP_UUID_LEN_128; i++)
ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i];
return ret;
}
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
ESPBTUUID ret;
if (data.length() == 4) {

View file

@ -20,6 +20,7 @@ class ESPBTUUID {
static ESPBTUUID from_uint32(uint32_t uuid);
static ESPBTUUID from_raw(const uint8_t *data);
static ESPBTUUID from_raw_reversed(const uint8_t *data);
static ESPBTUUID from_raw(const std::string &data);

View file

@ -462,14 +462,16 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
}
for (auto &data : this->manufacturer_datas_) {
ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str());
if (this->get_ibeacon().has_value()) {
auto ibeacon = this->get_ibeacon().value();
ESP_LOGVV(TAG, " iBeacon data:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str());
ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power());
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str());
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else {
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(),
format_hex_pretty(data.data).c_str());
}
}
for (auto &data : this->service_datas_) {
@ -478,7 +480,7 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
}
ESP_LOGVV(TAG, "Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str());
ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str());
#endif
}
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {

View file

@ -44,10 +44,10 @@ class ESPBLEiBeacon {
ESPBLEiBeacon(const uint8_t *data);
static optional<ESPBLEiBeacon> from_manufacturer_data(const ServiceData &data);
uint16_t get_major() { return ((this->beacon_data_.major & 0xFF) << 8) | (this->beacon_data_.major >> 8); }
uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); }
uint16_t get_major() { return byteswap(this->beacon_data_.major); }
uint16_t get_minor() { return byteswap(this->beacon_data_.minor); }
int8_t get_signal_power() { return this->beacon_data_.signal_power; }
ESPBTUUID get_uuid() { return ESPBTUUID::from_raw(this->beacon_data_.proximity_uuid); }
ESPBTUUID get_uuid() { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); }
protected:
struct {

View file

@ -140,6 +140,8 @@ CONF_TEST_PATTERN = "test_pattern"
# framerates
CONF_MAX_FRAMERATE = "max_framerate"
CONF_IDLE_FRAMERATE = "idle_framerate"
# frame buffer
CONF_FRAME_BUFFER_COUNT = "frame_buffer_count"
# stream trigger
CONF_ON_STREAM_START = "on_stream_start"
@ -213,6 +215,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
cv.framerate, cv.Range(min=0, max=1)
),
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -285,6 +288,7 @@ async def to_code(config):
cg.add(var.set_idle_update_interval(0))
else:
cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE]))
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
cg.add_define("USE_ESP32_CAMERA")

View file

@ -127,7 +127,7 @@ void ESP32Camera::dump_config() {
sensor_t *s = esp_camera_sensor_get();
auto st = s->status;
ESP_LOGCONFIG(TAG, " JPEG Quality: %u", st.quality);
// ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count);
ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count);
ESP_LOGCONFIG(TAG, " Contrast: %d", st.contrast);
ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness);
ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation);
@ -212,6 +212,8 @@ ESP32Camera::ESP32Camera() {
this->config_.frame_size = FRAMESIZE_VGA; // 640x480
this->config_.jpeg_quality = 10;
this->config_.fb_count = 1;
this->config_.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
this->config_.fb_location = CAMERA_FB_IN_PSRAM;
global_esp32_camera = this;
}
@ -333,6 +335,12 @@ void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
this->idle_update_interval_ = idle_update_interval;
}
/* set frame buffer parameters */
void ESP32Camera::set_frame_buffer_mode(camera_grab_mode_t mode) { this->config_.grab_mode = mode; }
void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) {
this->config_.fb_count = fb_count;
this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY);
}
/* ---------------- public API (specific) ---------------- */
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) {

View file

@ -145,6 +145,9 @@ class ESP32Camera : public Component, public EntityBase {
/* -- framerates */
void set_max_update_interval(uint32_t max_update_interval);
void set_idle_update_interval(uint32_t idle_update_interval);
/* -- frame buffer */
void set_frame_buffer_mode(camera_grab_mode_t mode);
void set_frame_buffer_count(uint8_t fb_count);
/* public API (derivated) */
void setup() override;

View file

@ -1,16 +1,16 @@
import esphome.config_validation as cv
import esphome.final_validate as fv
import esphome.codegen as cg
from esphome import pins
from esphome.const import CONF_ID
import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
)
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
@ -25,16 +25,26 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
CONF_I2S_AUDIO = "i2s_audio"
CONF_I2S_AUDIO_ID = "i2s_audio_id"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_I2S_MODE = "i2s_mode"
CONF_PRIMARY = "primary"
CONF_SECONDARY = "secondary"
CONF_USE_APLL = "use_apll"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_BITS_PER_CHANNEL = "bits_per_channel"
CONF_MONO = "mono"
CONF_LEFT = "left"
CONF_RIGHT = "right"
CONF_STEREO = "stereo"
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component)
I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent))
I2SAudioOut = i2s_audio_ns.class_(
"I2SAudioOut", cg.Parented.template(I2SAudioComponent)
I2SAudioBase = i2s_audio_ns.class_(
"I2SAudioBase", cg.Parented.template(I2SAudioComponent)
)
I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", I2SAudioBase)
I2SAudioOut = i2s_audio_ns.class_("I2SAudioOut", I2SAudioBase)
i2s_mode_t = cg.global_ns.enum("i2s_mode_t")
I2S_MODE_OPTIONS = {
@ -50,6 +60,75 @@ I2S_PORTS = {
VARIANT_ESP32C3: 1,
}
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
I2S_CHANNELS = {
CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT,
CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT,
}
i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t")
I2S_BITS_PER_SAMPLE = {
8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT,
16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT,
24: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_24BIT,
32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT,
}
i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t")
I2S_BITS_PER_CHANNEL = {
"default": i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT,
8: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_8BIT,
16: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_16BIT,
24: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_24BIT,
32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT,
}
_validate_bits = cv.float_with_unit("bits", "bit")
def i2s_audio_component_schema(
class_: MockObjClass,
*,
default_sample_rate: int,
default_channel: str,
default_bits_per_sample: str,
):
return cv.Schema(
{
cv.GenerateID(): cv.declare_id(class_),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Optional(CONF_CHANNEL, default=default_channel): cv.enum(I2S_CHANNELS),
cv.Optional(CONF_SAMPLE_RATE, default=default_sample_rate): cv.int_range(
min=1
),
cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All(
_validate_bits, cv.enum(I2S_BITS_PER_SAMPLE)
),
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
I2S_MODE_OPTIONS, lower=True
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All(
cv.Any(cv.float_with_unit("bits", "bit"), "default"),
cv.enum(I2S_BITS_PER_CHANNEL),
),
}
)
async def register_i2s_audio_component(var, config):
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
cg.add(var.set_i2s_mode(config[CONF_I2S_MODE]))
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_bits_per_channel(config[CONF_BITS_PER_CHANNEL]))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioComponent),

View file

@ -11,9 +11,27 @@ namespace i2s_audio {
class I2SAudioComponent;
class I2SAudioIn : public Parented<I2SAudioComponent> {};
class I2SAudioBase : public Parented<I2SAudioComponent> {
public:
void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; }
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
class I2SAudioOut : public Parented<I2SAudioComponent> {};
protected:
i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_;
i2s_bits_per_chan_t bits_per_channel_;
bool use_apll_;
};
class I2SAudioIn : public I2SAudioBase {};
class I2SAudioOut : public I2SAudioBase {};
class I2SAudioComponent : public Component {
public:

View file

@ -12,6 +12,10 @@ from .. import (
I2SAudioOut,
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
CONF_LEFT,
CONF_RIGHT,
CONF_MONO,
CONF_STEREO,
)
CODEOWNERS = ["@jesserockz"]
@ -30,12 +34,12 @@ CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
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,
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO]
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]

View file

@ -23,7 +23,7 @@ enum I2SState : uint8_t {
I2S_STATE_STOPPING,
};
class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, public I2SAudioOut {
class I2SAudioMediaPlayer : public Component, public Parented<I2SAudioComponent>, public media_player::MediaPlayer {
public:
void setup() override;
float get_setup_priority() const override { return esphome::setup_priority::LATE; }

View file

@ -1,20 +1,17 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome import pins
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER, CONF_SAMPLE_RATE
from esphome.components import microphone, esp32
import esphome.codegen as cg
from esphome.components import esp32, microphone
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER
from .. import (
CONF_I2S_MODE,
CONF_PRIMARY,
I2S_MODE_OPTIONS,
i2s_audio_ns,
I2SAudioComponent,
I2SAudioIn,
CONF_I2S_AUDIO_ID,
CONF_I2S_DIN_PIN,
CONF_RIGHT,
I2SAudioIn,
i2s_audio_component_schema,
i2s_audio_ns,
register_i2s_audio_component,
)
CODEOWNERS = ["@jesserockz"]
@ -23,29 +20,14 @@ DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_PDM = "pdm"
CONF_BITS_PER_SAMPLE = "bits_per_sample"
CONF_USE_APLL = "use_apll"
I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
)
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
CHANNELS = {
"left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
"right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
}
i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t")
BITS_PER_SAMPLE = {
16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT,
32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT,
}
INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
_validate_bits = cv.float_with_unit("bits", "bit")
def validate_esp32_variant(config):
variant = esp32.get_esp32_variant()
@ -62,21 +44,15 @@ def validate_esp32_variant(config):
BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All(
_validate_bits, cv.enum(BITS_PER_SAMPLE)
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
I2S_MODE_OPTIONS, lower=True
),
}
i2s_audio_component_schema(
I2SAudioMicrophone,
default_sample_rate=16000,
default_channel=CONF_RIGHT,
default_bits_per_sample="32bit",
)
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
@ -88,7 +64,7 @@ CONFIG_SCHEMA = cv.All(
"external": BASE_SCHEMA.extend(
{
cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_PDM): cv.boolean,
cv.Optional(CONF_PDM, default=False): cv.boolean,
}
),
},
@ -101,8 +77,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
await register_i2s_audio_component(var, config)
await microphone.register_microphone(var, config)
if config[CONF_ADC_TYPE] == "internal":
variant = esp32.get_esp32_variant()
@ -112,11 +88,3 @@ async def to_code(config):
else:
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM]))
cg.add(var.set_i2s_mode(config[CONF_I2S_MODE]))
cg.add(var.set_channel(config[CONF_CHANNEL]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
await microphone.register_microphone(var, config)

View file

@ -58,7 +58,7 @@ void I2SAudioMicrophone::start_() {
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
.bits_per_chan = this->bits_per_channel_,
};
esp_err_t err;
@ -167,19 +167,22 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
return 0;
}
this->status_clear_warning();
if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) {
// ESP-IDF I2S implementation right-extends 8-bit data to 16 bits,
// and 24-bit data to 32 bits.
switch (this->bits_per_sample_) {
case I2S_BITS_PER_SAMPLE_8BIT:
case I2S_BITS_PER_SAMPLE_16BIT:
return bytes_read;
} else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) {
std::vector<int16_t> samples;
case I2S_BITS_PER_SAMPLE_24BIT:
case I2S_BITS_PER_SAMPLE_32BIT: {
size_t samples_read = bytes_read / sizeof(int32_t);
samples.resize(samples_read);
for (size_t i = 0; i < samples_read; i++) {
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
samples[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
buf[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
}
memcpy(buf, samples.data(), samples_read * sizeof(int16_t));
return samples_read * sizeof(int16_t);
} else {
}
default:
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
return 0;
}

View file

@ -30,13 +30,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
}
#endif
void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
protected:
void start_();
void stop_();
@ -48,11 +41,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
bool adc_{false};
#endif
bool pdm_{false};
i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_;
uint32_t sample_rate_;
i2s_bits_per_sample_t bits_per_sample_;
bool use_apll_;
HighFrequencyLoopRequester high_freq_;
};

View file

@ -1,15 +1,19 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_MODE
import esphome.codegen as cg
from esphome.components import esp32, speaker
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT
from .. import (
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
I2SAudioComponent,
CONF_LEFT,
CONF_MONO,
CONF_RIGHT,
CONF_STEREO,
I2SAudioOut,
i2s_audio_component_schema,
i2s_audio_ns,
register_i2s_audio_component,
)
CODEOWNERS = ["@jesserockz"]
@ -19,19 +23,16 @@ I2SAudioSpeaker = i2s_audio_ns.class_(
"I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
CONF_MUTE_PIN = "mute_pin"
CONF_DAC_TYPE = "dac_type"
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
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,
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
@ -44,28 +45,40 @@ def validate_esp32_variant(config):
return config
BASE_SCHEMA = (
speaker.SPEAKER_SCHEMA.extend(
i2s_audio_component_schema(
I2SAudioSpeaker,
default_sample_rate=16000,
default_channel=CONF_MONO,
default_bits_per_sample="16bit",
)
)
.extend(
{
cv.Optional(
CONF_TIMEOUT, default="100ms"
): cv.positive_time_period_milliseconds,
}
)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": speaker.SPEAKER_SCHEMA.extend(
"internal": BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioSpeaker),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
).extend(cv.COMPONENT_SCHEMA),
"external": speaker.SPEAKER_SCHEMA.extend(
),
"external": BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioSpeaker),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
*EXTERNAL_DAC_OPTIONS, lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
),
},
key=CONF_DAC_TYPE,
),
@ -76,12 +89,11 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await register_i2s_audio_component(var, config)
await speaker.register_speaker(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
cg.add(var.set_timeout(config[CONF_TIMEOUT]))

View file

@ -56,6 +56,21 @@ void I2SAudioSpeaker::start_() {
this->task_created_ = true;
}
template<typename a, typename b> const uint8_t *convert_data_format(const a *from, b *to, size_t &bytes, bool repeat) {
if (sizeof(a) == sizeof(b) && !repeat) {
return reinterpret_cast<const uint8_t *>(from);
}
const b *result = to;
for (size_t i = 0; i < bytes; i += sizeof(a)) {
b value = static_cast<b>(*from++) << (sizeof(b) - sizeof(a)) * 8;
*to++ = value;
if (repeat)
*to++ = value;
}
bytes *= (sizeof(b) / sizeof(a)) * (repeat ? 2 : 1); // NOLINT
return reinterpret_cast<const uint8_t *>(result);
}
void I2SAudioSpeaker::player_task(void *params) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
@ -64,19 +79,19 @@ void I2SAudioSpeaker::player_task(void *params) {
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.mode = (i2s_mode_t) (this_speaker->i2s_mode_ | I2S_MODE_TX),
.sample_rate = this_speaker->sample_rate_,
.bits_per_sample = this_speaker->bits_per_sample_,
.channel_format = this_speaker->channel_,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 128,
.use_apll = false,
.dma_buf_len = 256,
.use_apll = this_speaker->use_apll_,
.tx_desc_auto_clear = true,
.fixed_mclk = I2S_PIN_NO_CHANGE,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
.bits_per_chan = this_speaker->bits_per_channel_,
};
#if SOC_I2S_SUPPORTS_DAC
if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
@ -114,10 +129,11 @@ void I2SAudioSpeaker::player_task(void *params) {
event.type = TaskEventType::STARTED;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
int16_t buffer[BUFFER_SIZE / 2];
int32_t buffer[BUFFER_SIZE];
while (true) {
if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 100 / portTICK_PERIOD_MS) != pdTRUE) {
if (xQueueReceive(this_speaker->buffer_queue_, &data_event, this_speaker->timeout_ / portTICK_PERIOD_MS) !=
pdTRUE) {
break; // End of audio from main thread
}
if (data_event.stop) {
@ -125,17 +141,28 @@ void I2SAudioSpeaker::player_task(void *params) {
xQueueReset(this_speaker->buffer_queue_); // Flush queue
break;
}
const uint8_t *data = data_event.data;
size_t remaining = data_event.len;
switch (this_speaker->bits_per_sample_) {
case I2S_BITS_PER_SAMPLE_8BIT:
case I2S_BITS_PER_SAMPLE_16BIT: {
data = convert_data_format(reinterpret_cast<const int16_t *>(data), reinterpret_cast<int16_t *>(buffer),
remaining, this_speaker->channel_ == I2S_CHANNEL_FMT_ALL_LEFT);
break;
}
case I2S_BITS_PER_SAMPLE_24BIT:
case I2S_BITS_PER_SAMPLE_32BIT: {
data = convert_data_format(reinterpret_cast<const int16_t *>(data), reinterpret_cast<int32_t *>(buffer),
remaining, this_speaker->channel_ == I2S_CHANNEL_FMT_ALL_LEFT);
break;
}
}
while (remaining != 0) {
size_t bytes_written;
memmove(buffer, data_event.data, data_event.len);
size_t remaining = data_event.len / 2;
size_t current = 0;
while (remaining > 0) {
uint32_t sample = (buffer[current] << 16) | (buffer[current] & 0xFFFF);
esp_err_t err = i2s_write(this_speaker->parent_->get_port(), &sample, sizeof(sample), &bytes_written,
(10 / portTICK_PERIOD_MS));
esp_err_t err =
i2s_write(this_speaker->parent_->get_port(), data, remaining, &bytes_written, (32 / portTICK_PERIOD_MS));
if (err != ESP_OK) {
event = {.type = TaskEventType::WARNING, .err = err};
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
@ -143,21 +170,8 @@ void I2SAudioSpeaker::player_task(void *params) {
}
continue;
}
if (bytes_written != sizeof(sample)) {
event = {.type = TaskEventType::WARNING, .err = ESP_FAIL};
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGW(TAG, "Failed to send WARNING event");
}
continue;
}
remaining--;
current++;
}
event.type = TaskEventType::PLAYING;
event.err = current;
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGW(TAG, "Failed to send PLAYING event");
data += bytes_written;
remaining -= bytes_written;
}
}
@ -213,13 +227,11 @@ void I2SAudioSpeaker::watch_() {
case TaskEventType::STARTED:
ESP_LOGD(TAG, "Started I2S Audio Speaker");
this->state_ = speaker::STATE_RUNNING;
this->status_clear_warning();
break;
case TaskEventType::STOPPING:
ESP_LOGD(TAG, "Stopping I2S Audio Speaker");
break;
case TaskEventType::PLAYING:
this->status_clear_warning();
break;
case TaskEventType::STOPPED:
this->state_ = speaker::STATE_STOPPED;
vTaskDelete(this->player_task_handle_);

View file

@ -21,7 +21,6 @@ static const size_t BUFFER_SIZE = 1024;
enum class TaskEventType : uint8_t {
STARTING = 0,
STARTED,
PLAYING,
STOPPING,
STOPPED,
WARNING = 255,
@ -38,18 +37,18 @@ struct DataEvent {
uint8_t data[BUFFER_SIZE];
};
class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAudioOut {
class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component {
public:
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
void setup() override;
void loop() override;
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
#if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif
void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
void start() override;
void stop() override;
@ -70,13 +69,13 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
QueueHandle_t buffer_queue_;
QueueHandle_t event_queue_;
uint32_t timeout_{0};
uint8_t dout_pin_{0};
bool task_created_{false};
#if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif
uint8_t external_dac_channels_;
};
} // namespace i2s_audio

View file

@ -1,18 +1,17 @@
from __future__ import annotations
import logging
import hashlib
import io
import logging
from pathlib import Path
import re
from magic import Magic
from esphome import core
from esphome.components import font
from esphome import external_files
import esphome.config_validation as cv
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components import font
import esphome.config_validation as cv
from esphome.const import (
CONF_DITHER,
CONF_FILE,
@ -239,12 +238,11 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
def load_svg_image(file: bytes, resize: tuple[int, int]):
# Local import only to allow "validate_pillow_installed" to run *before* importing it
from PIL import Image
# This import is only needed in case of SVG images; adding it
# to the top would force configurations not using SVG to also have it
# installed for no reason.
from cairosvg import svg2png
from PIL import Image
if resize:
req_width, req_height = resize
@ -274,6 +272,9 @@ async def to_code(config):
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}")
try:
with open(path, "rb") as f:
file_contents = f.read()

View file

@ -170,13 +170,11 @@ def _notify_old_style(config):
return config
# NOTE: Keep this in mind when updating the recommended version:
# * For all constants below, update platformio.ini (in this repo)
# The dev and latest branches will be at *least* this version, which is what matters.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 7, 0), "libretiny"),
"recommended": (cv.Version(1, 5, 1), None),
"recommended": (cv.Version(1, 7, 0), None),
}

View file

@ -22,8 +22,9 @@ from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, focused_widgets, update_to_code
from .defines import CONF_ADJUSTABLE, CONF_SKIP
from .defines import add_define
from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
from .gradient import GRADIENT_SCHEMA, gradients_to_code
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
from .schemas import (
@ -128,17 +129,6 @@ for w_type in WIDGET_TYPES.values():
)(update_to_code)
lv_defines = {} # Dict of #defines to provide as build flags
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
lv_defines[macro] = value
def as_macro(macro, value):
if value is None:
return f"#define {macro}"
@ -153,14 +143,14 @@ LV_CONF_H_FORMAT = """\
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in lv_defines.items()]
definitions = [as_macro(m, v) for m, v in df.lv_defines.items()]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
def final_validation(config):
if pages := config.get(CONF_PAGES):
if all(p[CONF_SKIP] for p in pages):
if all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]:
@ -185,7 +175,7 @@ def final_validation(config):
for w in focused_widgets:
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
if CONF_ADJUSTABLE in widget_conf and not widget_conf[CONF_ADJUSTABLE]:
if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]:
raise cv.Invalid(
"A non adjustable arc may not be focused",
path,
@ -268,6 +258,7 @@ async def to_code(config):
await encoders_to_code(lv_component, config)
await theme_to_code(config)
await styles_to_code(config)
await gradients_to_code(config)
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config)
@ -351,6 +342,7 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_THEME): cv.Schema(
{cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()}
),
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),

View file

@ -4,6 +4,8 @@ Constants already defined in esphome.const are not duplicated here and must be i
"""
import logging
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import Lambda
@ -13,8 +15,19 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from .helpers import requires_component
LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")
lv_defines = {} # Dict of #defines to provide as build flags
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
lv_defines[macro] = value
def literal(arg):
if isinstance(arg, str):
@ -173,6 +186,9 @@ LV_ANIM = LvConstant(
"OUT_BOTTOM",
)
LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")
LOG_LEVELS = (
"TRACE",
"INFO",
@ -406,6 +422,7 @@ CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow"
CONF_FREEZE = "freeze"
CONF_FULL_REFRESH = "full_refresh"
CONF_GRADIENTS = "gradients"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span"

View file

@ -0,0 +1,61 @@
from esphome import config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_COLOR,
CONF_DIRECTION,
CONF_DITHER,
CONF_ID,
CONF_POSITION,
)
from esphome.cpp_generator import MockObj
from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define
from .lv_validation import lv_color, lv_fraction
from .lvcode import lv_assign
from .types import lv_gradient_t
CONF_STOPS = "stops"
def min_stops(value):
if len(value) < 2:
raise cv.Invalid("Must have at least 2 stops")
return value
GRADIENT_SCHEMA = cv.ensure_list(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of,
cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of,
cv.Required(CONF_STOPS): cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Required(CONF_POSITION): lv_fraction,
}
)
],
min_stops,
),
}
)
)
async def gradients_to_code(config):
max_stops = 2
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
max_stops = max(max_stops, len(gradient[CONF_STOPS]))
lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION]))
lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER]))
lv_assign(var.stops_count, len(gradient[CONF_STOPS]))
for index, stop in enumerate(gradient[CONF_STOPS]):
lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR]))
lv_assign(
var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION])
)
add_define("LV_GRADIENT_MAX_STOPS", max_stops)

View file

@ -1,12 +1,19 @@
from typing import Union
import esphome.codegen as cg
from esphome.components.color import ColorStruct
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
from esphome.components.font import Font
from esphome.components.image import Image_
import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE
from esphome.core import HexInt, Lambda
from esphome.const import (
CONF_ARGS,
CONF_COLOR,
CONF_FORMAT,
CONF_ID,
CONF_TIME,
CONF_VALUE,
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.cpp_types import ESPTime, uint32
from esphome.helpers import cpp_string_escape
@ -23,14 +30,9 @@ from .defines import (
call_lambda,
literal,
)
from .helpers import (
esphome_fonts_used,
lv_fonts_used,
lvgl_components_required,
requires_component,
)
from .helpers import esphome_fonts_used, lv_fonts_used, requires_component
from .lvcode import lv_expr
from .types import lv_font_t, lv_img_t
from .types import lv_font_t, lv_gradient_t, lv_img_t
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@ -59,11 +61,17 @@ def color_retmapper(value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if isinstance(value, int):
hexval = HexInt(value)
return lv_expr.color_hex(hexval)
# Must be an id
lvgl_components_required.add(CONF_COLOR)
return lv_expr.color_from(MockObj(value))
return literal(
f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})"
)
if isinstance(value, ID):
cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0]
if CONF_HEX in cval:
r, g, b = cval[CONF_HEX]
else:
r, g, b, _ = from_rgbw(cval)
return literal(f"lv_color_make({r}, {g}, {b})")
assert False
def option_string(value):
@ -132,7 +140,7 @@ radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
@schema_extractor("one_of")
def radius_validator(value):
def fraction_validator(value):
if value == SCHEMA_EXTRACT:
return radius_consts.choices
value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
@ -141,7 +149,7 @@ def radius_validator(value):
return value
radius = LValidator(radius_validator, uint32, retmapper=literal)
lv_fraction = LValidator(fraction_validator, uint32, retmapper=literal)
def id_name(value):
@ -242,6 +250,21 @@ lv_int = LValidator(cv.int_, cg.int_)
lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
def gradient_mapper(value):
return MockObj(value)
def gradient_validator(value):
return cv.use_id(lv_gradient_t)(value)
lv_gradient = LValidator(
validator=gradient_validator,
rtype=lv_gradient_t,
retmapper=gradient_mapper,
)
def is_lv_font(font):
return isinstance(font, str) and font.lower() in LV_FONTS

View file

@ -184,6 +184,7 @@ class LvContext(LambdaContext):
self.lv_component = lv_component
async def add_init_lambda(self):
if self.code_list:
cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
LvContext.added_lambda_count += 1

View file

@ -42,9 +42,6 @@ extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT
extern std::string lv_event_code_name_for(uint8_t event_code);
extern bool lv_is_pre_initialise();
#ifdef USE_LVGL_COLOR
inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif // USE_LVGL_COLOR
#if LV_COLOR_DEPTH == 16
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
#elif LV_COLOR_DEPTH == 32

View file

@ -17,9 +17,9 @@ from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_image
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image
from .lvcode import LvglComponent, lv_event_t_ptr
from .types import (
LVEncoderListener,
@ -94,9 +94,10 @@ STYLE_PROPS = {
"arc_width": cv.positive_int,
"anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color,
"bg_grad": lv_gradient,
"bg_grad_color": lvalid.lv_color,
"bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of,
"bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of,
"bg_grad_dir": LV_GRAD_DIR.one_of,
"bg_grad_stop": lvalid.stop_value,
"bg_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color,
@ -160,7 +161,7 @@ STYLE_PROPS = {
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": lvalid.radius,
"radius": lvalid.lv_fraction,
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,

View file

@ -59,6 +59,7 @@ LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t")
lv_page_t = LvType("LvPageType", parents=(LvCompound,))
lv_img_t = LvType("lv_img_t")
lv_gradient_t = LvType("lv_grad_dsc_t")
LV_EVENT = MockObj(base="LV_EVENT_", op="")
LV_STATE = MockObj(base="LV_STATE_", op="")

View file

@ -5,6 +5,7 @@ from esphome.const import (
CONF_COLOR,
CONF_COUNT,
CONF_ID,
CONF_ITEMS,
CONF_LENGTH,
CONF_LOCAL,
CONF_RANGE_FROM,
@ -17,6 +18,7 @@ from esphome.const import (
from ..automation import action_to_code
from ..defines import (
CONF_END_VALUE,
CONF_INDICATOR,
CONF_MAIN,
CONF_PIVOT_X,
CONF_PIVOT_Y,
@ -165,7 +167,12 @@ METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)}
class MeterType(WidgetType):
def __init__(self):
super().__init__(CONF_METER, lv_meter_t, (CONF_MAIN,), METER_SCHEMA)
super().__init__(
CONF_METER,
lv_meter_t,
(CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS),
METER_SCHEMA,
)
async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters"""

View file

@ -32,6 +32,12 @@ void MAX31856Sensor::dump_config() {
LOG_PIN(" CS Pin: ", this->cs_);
ESP_LOGCONFIG(TAG, " Mains Filter: %s",
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) {
ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown");
} else {
ESP_LOGCONFIG(TAG, " Thermocouple Type: %c", "BEJKNRST"[this->thermocouple_type_]);
}
LOG_UPDATE_INTERVAL(this);
}
@ -129,7 +135,12 @@ void MAX31856Sensor::clear_fault_() {
}
void MAX31856Sensor::set_thermocouple_type_() {
MAX31856ThermocoupleType type = MAX31856_TCTYPE_K;
MAX31856ThermocoupleType type;
if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) {
type = MAX31856_TCTYPE_K;
} else {
type = this->thermocouple_type_;
}
ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type);
uint8_t t = this->read_register_(MAX31856_CR1_REG);
t &= 0xF0; // mask off bottom 4 bits

View file

@ -50,7 +50,6 @@ enum MAX31856Registers {
/**
* Multiple types of thermocouples supported by the chip.
* Currently only K type implemented here.
*/
enum MAX31856ThermocoupleType {
MAX31856_TCTYPE_B = 0b0000, // 0x00
@ -78,11 +77,15 @@ class MAX31856Sensor : public sensor::Sensor,
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void set_filter(MAX31856ConfigFilter filter) { filter_ = filter; }
void set_filter(MAX31856ConfigFilter filter) { this->filter_ = filter; }
void set_thermocouple_type(MAX31856ThermocoupleType thermocouple_type) {
this->thermocouple_type_ = thermocouple_type;
}
void update() override;
protected:
MAX31856ConfigFilter filter_;
MAX31856ThermocoupleType thermocouple_type_;
uint8_t read_register_(uint8_t reg);
uint32_t read_register24_(uint8_t reg);

View file

@ -3,6 +3,7 @@ from esphome.components import sensor, spi
import esphome.config_validation as cv
from esphome.const import (
CONF_MAINS_FILTER,
CONF_THERMOCOUPLE_TYPE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
@ -18,6 +19,17 @@ FILTER = {
50: MAX31865ConfigFilter.FILTER_50HZ,
60: MAX31865ConfigFilter.FILTER_60HZ,
}
MAX31856ThermocoupleType = max31856_ns.enum("MAX31856ThermocoupleType")
THERMOCOUPLE_TYPE = {
"B": MAX31856ThermocoupleType.MAX31856_TCTYPE_B,
"E": MAX31856ThermocoupleType.MAX31856_TCTYPE_E,
"J": MAX31856ThermocoupleType.MAX31856_TCTYPE_J,
"K": MAX31856ThermocoupleType.MAX31856_TCTYPE_K,
"N": MAX31856ThermocoupleType.MAX31856_TCTYPE_N,
"R": MAX31856ThermocoupleType.MAX31856_TCTYPE_R,
"S": MAX31856ThermocoupleType.MAX31856_TCTYPE_S,
"T": MAX31856ThermocoupleType.MAX31856_TCTYPE_T,
}
CONFIG_SCHEMA = (
sensor.sensor_schema(
@ -34,6 +46,13 @@ CONFIG_SCHEMA = (
),
}
)
.extend(
{
cv.Optional(CONF_THERMOCOUPLE_TYPE, default="K"): cv.enum(
THERMOCOUPLE_TYPE, upper=True, space=""
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema())
)
@ -44,3 +63,4 @@ async def to_code(config):
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
cg.add(var.set_filter(config[CONF_MAINS_FILTER]))
cg.add(var.set_thermocouple_type(config[CONF_THERMOCOUPLE_TYPE]))

View file

@ -1,14 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_THERMOCOUPLE_TYPE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
CONF_THERMOCOUPLE_TYPE = "thermocouple_type"
CONF_HOT_JUNCTION = "hot_junction"
CONF_COLD_JUNCTION = "cold_junction"

View file

@ -37,6 +37,7 @@ struct MediaPlayerSupportedFormat {
uint32_t sample_rate;
uint32_t num_channels;
MediaPlayerFormatPurpose purpose;
uint32_t sample_bytes;
};
class MediaPlayer;

View file

@ -1,26 +1,29 @@
import binascii
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
import esphome.codegen as cg
from esphome.components import modbus
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_NAME,
CONF_LAMBDA,
CONF_NAME,
CONF_OFFSET,
CONF_TRIGGER_ID,
)
from esphome.cpp_helpers import logging
from .const import (
CONF_ALLOW_DUPLICATE_COMMANDS,
CONF_BITMASK,
CONF_BYTE_OFFSET,
CONF_COMMAND_THROTTLE,
CONF_OFFLINE_SKIP_UPDATES,
CONF_CUSTOM_COMMAND,
CONF_FORCE_NEW_RANGE,
CONF_MAX_CMD_RETRIES,
CONF_MODBUS_CONTROLLER_ID,
CONF_OFFLINE_SKIP_UPDATES,
CONF_ON_COMMAND_SENT,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
@ -131,6 +134,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_COMMAND_THROTTLE, default="0ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(
CONF_SERVER_REGISTERS,
@ -257,6 +261,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config:
for server_register in config[CONF_SERVER_REGISTERS]:

View file

@ -1,16 +1,16 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,

View file

@ -5,6 +5,7 @@ CONF_COMMAND_THROTTLE = "command_throttle"
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
CONF_CUSTOM_COMMAND = "custom_command"
CONF_FORCE_NEW_RANGE = "force_new_range"
CONF_MAX_CMD_RETRIES = "max_cmd_retries"
CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
CONF_ON_COMMAND_SENT = "on_command_sent"

View file

@ -18,11 +18,11 @@ void ModbusController::setup() { this->create_register_ranges_(); }
bool ModbusController::send_next_command_() {
uint32_t last_send = millis() - this->last_command_timestamp_;
if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) {
auto &command = command_queue_.front();
if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) {
auto &command = this->command_queue_.front();
// remove from queue if command was sent too often
if (command->send_countdown < 1) {
if (!command->should_retry(this->max_cmd_retries_)) {
if (!this->module_offline_) {
ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
@ -34,11 +34,9 @@ bool ModbusController::send_next_command_() {
}
}
this->module_offline_ = true;
ESP_LOGD(
TAG,
"Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue",
this->address_, command->register_address, command->send_countdown);
command_queue_.pop_front();
ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue",
this->address_, command->register_address);
this->command_queue_.pop_front();
} else {
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
command->register_address, command->register_count);
@ -50,11 +48,11 @@ bool ModbusController::send_next_command_() {
// remove from queue if no handler is defined
if (!command->on_data_func) {
command_queue_.pop_front();
this->command_queue_.pop_front();
}
}
}
return (!command_queue_.empty());
return (!this->command_queue_.empty());
}
// Queue incoming response
@ -77,7 +75,7 @@ void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
current_command->payload = data;
this->incoming_queue_.push(std::move(current_command));
ESP_LOGV(TAG, "Modbus response queued");
command_queue_.pop_front();
this->command_queue_.pop_front();
}
}
@ -99,7 +97,7 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
"payload size=%zu",
function_code, current_command->register_address, current_command->register_count,
current_command->payload.size());
command_queue_.pop_front();
this->command_queue_.pop_front();
}
}
@ -178,7 +176,7 @@ void ModbusController::queue_command(const ModbusCommandItem &command) {
if (!this->allow_duplicate_commands_) {
// check if this command is already qeued.
// not very effective but the queue is never really large
for (auto &item : command_queue_) {
for (auto &item : this->command_queue_) {
if (item->is_equal(command)) {
ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u",
static_cast<uint8_t>(command.register_type), command.register_address, command.register_count);
@ -189,7 +187,7 @@ void ModbusController::queue_command(const ModbusCommandItem &command) {
}
}
}
command_queue_.push_back(make_unique<ModbusCommandItem>(command));
this->command_queue_.push_back(make_unique<ModbusCommandItem>(command));
}
void ModbusController::update_range_(RegisterRange &r) {
@ -224,8 +222,8 @@ void ModbusController::update_range_(RegisterRange &r) {
// Once we get a response to the command it is removed from the queue and the next command is send
//
void ModbusController::update() {
if (!command_queue_.empty()) {
ESP_LOGV(TAG, "%zu modbus commands already in queue", command_queue_.size());
if (!this->command_queue_.empty()) {
ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size());
} else {
ESP_LOGV(TAG, "Updating modbus component");
}
@ -346,6 +344,8 @@ size_t ModbusController::create_register_ranges_() {
void ModbusController::dump_config() {
ESP_LOGCONFIG(TAG, "ModbusController:");
ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
ESP_LOGCONFIG(TAG, " Max Command Retries: %d", this->max_cmd_retries_);
ESP_LOGCONFIG(TAG, " Offline Skip Updates: %d", this->offline_skip_updates_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "sensormap");
for (auto &it : sensorset_) {
@ -560,8 +560,9 @@ bool ModbusCommandItem::send() {
} else {
modbusdevice->send_raw(this->payload);
}
ESP_LOGV(TAG, "Command sent %d 0x%X %d", uint8_t(this->function_code), this->register_address, this->register_count);
send_countdown--;
this->send_count_++;
ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address,
this->register_count, this->send_count_);
return true;
}

View file

@ -312,7 +312,6 @@ struct RegisterRange {
class ModbusCommandItem {
public:
static const size_t MAX_PAYLOAD_BYTES = 240;
static const uint8_t MAX_SEND_REPEATS = 5;
ModbusController *modbusdevice;
uint16_t register_address;
uint16_t register_count;
@ -322,9 +321,9 @@ class ModbusCommandItem {
on_data_func;
std::vector<uint8_t> payload = {};
bool send();
// wrong commands (esp. custom commands) can block the send queue
// limit the number of repeats
uint8_t send_countdown{MAX_SEND_REPEATS};
/// Check if the command should be retried based on the max_retries parameter
bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
/// factory methods
/** Create modbus read command
* Function code 02-04
@ -413,6 +412,11 @@ class ModbusCommandItem {
&&handler = nullptr);
bool is_equal(const ModbusCommandItem &other);
protected:
// wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
/// How many times this command has been sent
uint8_t send_count_{0};
};
/** Modbus controller class.
@ -464,6 +468,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
bool get_module_offline() { return module_offline_; }
/// Set callback for commands
void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
/// called by esphome generated code to set the max_cmd_retries.
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
/// get how many times a command will be (re)sent if no response is received
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
protected:
/// parse sensormap_ and create range of sequential addresses
@ -498,6 +506,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
bool module_offline_;
/// how many updates to skip if module is offline
uint16_t offline_skip_updates_;
/// How many times we will retry a command if we get no response
uint8_t max_cmd_retries_{4};
CallbackManager<void(int, int)> command_sent_callback_{};
};

View file

@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
@ -12,14 +12,13 @@ from esphome.const import (
from .. import (
MODBUS_WRITE_REGISTER_TYPE,
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
SENSOR_VALUE_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
)
from ..const import (
CONF_BITMASK,
CONF_CUSTOM_COMMAND,

View file

@ -1,20 +1,15 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import output
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_MULTIPLY,
)
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY
from .. import (
modbus_controller_ns,
modbus_calc_properties,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
SENSOR_VALUE_TYPE,
modbus_calc_properties,
modbus_controller_ns,
)
from ..const import (
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
@ -65,6 +60,7 @@ CONFIG_SCHEMA = cv.typed_schema(
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
# Binary Output
write_template = None
if config[CONF_REGISTER_TYPE] == "coil":
var = cg.new_Pvariable(
config[CONF_ID],
@ -72,7 +68,7 @@ async def to_code(config):
byte_offset,
)
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
write_template = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusBinaryOutput.operator("ptr"), "item"),
@ -92,7 +88,7 @@ async def to_code(config):
)
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
write_template = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusFloatOutput.operator("ptr"), "item"),
@ -105,5 +101,5 @@ async def to_code(config):
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_parent(parent))
if CONF_WRITE_LAMBDA in config:
cg.add(var.set_write_template(template_))
if write_template:
cg.add(var.set_write_template(write_template))

View file

@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
from .. import (

View file

@ -1,17 +1,17 @@
import esphome.codegen as cg
from esphome.components import sensor
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 (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,

View file

@ -1,17 +1,16 @@
import esphome.codegen as cg
from esphome.components import switch
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 (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,

View file

@ -1,26 +1,25 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
add_modbus_base_properties,
modbus_controller_ns,
modbus_calc_properties,
validate_modbus_register,
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
MODBUS_REGISTER_TYPE,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_RAW_ENCODE,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
CONF_RESPONSE_SIZE,
CONF_SKIP_UPDATES,
CONF_RAW_ENCODE,
CONF_REGISTER_TYPE,
)
DEPENDENCIES = ["modbus_controller"]

View file

@ -1,13 +1,7 @@
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option
import esphome.config_validation as cv
from esphome.const import (
CONF_ENABLE_IPV6,
CONF_MIN_IPV6_ADDR_COUNT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
@ -23,10 +17,17 @@ CONFIG_SCHEMA = cv.Schema(
esp8266=False,
esp32=False,
rp2040=False,
bk72xx=False,
): cv.All(
cv.boolean,
cv.Any(
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
cv.require_framework_version(
esp_idf=cv.Version(0, 0, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp8266_arduino=cv.Version(0, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0),
bk72xx_libretiny=cv.Version(1, 7, 0),
),
cv.boolean_false,
),
),
@ -53,3 +54,5 @@ async def to_code(config):
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
if CORE.is_bk72xx:
cg.add_build_flag("-DCONFIG_IPV6")

View 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))

View 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

View 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

View 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

View 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

View file

@ -1,31 +1,28 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
import esphome.codegen as cg
from esphome.components import display
from esphome.components.esp32 import const, only_on_variant
import esphome.config_validation as cv
from esphome.const import (
CONF_ENABLE_PIN,
CONF_HSYNC_PIN,
CONF_RESET_PIN,
CONF_BLUE,
CONF_COLOR_ORDER,
CONF_DATA_PINS,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_GREEN,
CONF_HEIGHT,
CONF_HSYNC_PIN,
CONF_ID,
CONF_IGNORE_STRAPPING_WARNING,
CONF_DIMENSIONS,
CONF_VSYNC_PIN,
CONF_WIDTH,
CONF_HEIGHT,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_COLOR_ORDER,
CONF_RED,
CONF_GREEN,
CONF_BLUE,
CONF_NUMBER,
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_INVERT_COLORS,
)
from esphome.components.esp32 import (
only_on_variant,
const,
CONF_RED,
CONF_RESET_PIN,
CONF_VSYNC_PIN,
CONF_WIDTH,
)
DEPENDENCIES = ["esp32"]

View file

@ -6,9 +6,14 @@ namespace esphome {
namespace rpi_dpi_rgb {
void RpiDpiRgb::setup() {
esph_log_config(TAG, "Setting up RPI_DPI_RGB");
ESP_LOGCONFIG(TAG, "Setting up RPI_DPI_RGB");
this->reset_display_();
esp_lcd_rgb_panel_config_t config{};
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.v_res = this->height_;
config.timings.hsync_pulse_width = this->hsync_pulse_width_;
@ -20,7 +25,6 @@ void RpiDpiRgb::setup() {
config.timings.flags.pclk_active_neg = this->pclk_inverted_;
config.timings.pclk_hz = this->pclk_frequency_;
config.clk_src = LCD_CLK_SRC_PLL160M;
config.sram_trans_align = 64;
config.psram_trans_align = 64;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) {
@ -34,11 +38,19 @@ void RpiDpiRgb::setup() {
config.pclk_gpio_num = this->pclk_pin_->get_pin();
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
if (err != ESP_OK) {
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
esph_log_config(TAG, "RPI_DPI_RGB setup complete");
ESP_LOGCONFIG(TAG, "RPI_DPI_RGB setup complete");
}
void RpiDpiRgb::loop() {
#if ESP_IDF_VERSION_MAJOR >= 5
if (this->handle_ != nullptr)
esp_lcd_rgb_panel_restart(this->handle_);
#endif // ESP_IDF_VERSION_MAJOR
}
void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
@ -53,7 +65,7 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
}
x_start += this->offset_x_;
y_start += this->offset_y_;
esp_err_t err;
esp_err_t err = ESP_OK;
// x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display.
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
@ -69,7 +81,7 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
}
}
if (err != ESP_OK)
esph_log_e(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
}
void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {

View file

@ -23,6 +23,7 @@ class RpiDpiRgb : public display::Display {
public:
void update() override { this->do_update_(); }
void setup() override;
void loop() override;
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;
void draw_pixel_at(int x, int y, Color color) override;

View file

@ -8,8 +8,22 @@ namespace st7701s {
void ST7701S::setup() {
esph_log_config(TAG, "Setting up ST7701S");
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{};
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.v_res = this->height_;
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.pclk_hz = this->pclk_frequency_;
config.clk_src = LCD_CLK_SRC_PLL160M;
config.sram_trans_align = 64;
config.psram_trans_align = 64;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
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.pclk_gpio_num = this->pclk_pin_->get_pin();
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) {
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");
}
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,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (w <= 0 || h <= 0)
@ -160,10 +179,7 @@ void ST7701S::write_init_sequence_() {
this->write_data_(val);
ESP_LOGD(TAG, "write MADCTL %X", val);
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
this->set_timeout(120, [this] {
this->write_command_(SLEEP_OUT);
this->write_command_(DISPLAY_ON);
});
this->set_timeout(120, [this] { this->complete_setup_(); });
}
void ST7701S::dump_config() {

View file

@ -33,6 +33,8 @@ class ST7701S : public display::Display,
public:
void update() override { this->do_update_(); }
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,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_MODE,
CONF_AWAY_CONFIG,
@ -15,15 +15,15 @@ from esphome.const import (
CONF_DRY_ACTION,
CONF_DRY_MODE,
CONF_FAN_MODE,
CONF_FAN_MODE_ON_ACTION,
CONF_FAN_MODE_OFF_ACTION,
CONF_FAN_MODE_AUTO_ACTION,
CONF_FAN_MODE_DIFFUSE_ACTION,
CONF_FAN_MODE_FOCUS_ACTION,
CONF_FAN_MODE_HIGH_ACTION,
CONF_FAN_MODE_LOW_ACTION,
CONF_FAN_MODE_MEDIUM_ACTION,
CONF_FAN_MODE_HIGH_ACTION,
CONF_FAN_MODE_MIDDLE_ACTION,
CONF_FAN_MODE_FOCUS_ACTION,
CONF_FAN_MODE_DIFFUSE_ACTION,
CONF_FAN_MODE_OFF_ACTION,
CONF_FAN_MODE_ON_ACTION,
CONF_FAN_MODE_QUIET_ACTION,
CONF_FAN_ONLY_ACTION,
CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER,
@ -50,8 +50,8 @@ from esphome.const import (
CONF_MIN_HEATING_RUN_TIME,
CONF_MIN_IDLE_TIME,
CONF_MIN_TEMPERATURE,
CONF_NAME,
CONF_MODE,
CONF_NAME,
CONF_OFF_MODE,
CONF_PRESET,
CONF_SENSOR,
@ -892,7 +892,7 @@ async def to_code(config):
if name.upper() in climate.CLIMATE_PRESETS:
standard_preset = climate.CLIMATE_PRESETS[name.upper()]
if two_points_available is True:
if two_points_available:
preset_target_config = ThermostatClimateTargetTempConfig(
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
@ -905,6 +905,8 @@ async def to_code(config):
preset_target_config = ThermostatClimateTargetTempConfig(
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
)
else:
preset_target_config = None
preset_target_variable = cg.new_variable(
preset_config[CONF_ID], preset_target_config

View file

@ -1,8 +1,9 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_KEY
from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID
from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns
TM1638Key = tm1638_ns.class_("TM1638Key", binary_sensor.BinarySensor)

View file

@ -1,13 +1,13 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
import esphome.codegen as cg
from esphome.components import display
import esphome.config_validation as cv
from esphome.const import (
CONF_CLK_PIN,
CONF_DIO_PIN,
CONF_ID,
CONF_INTENSITY,
CONF_LAMBDA,
CONF_CLK_PIN,
CONF_DIO_PIN,
CONF_STB_PIN,
)

View file

@ -1,8 +1,9 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LED
from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID
from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns
TM1638OutputLed = tm1638_ns.class_("TM1638OutputLed", output.BinaryOutput, cg.Component)

View file

@ -1,8 +1,9 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_LED
from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID
from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns
TM1638SwitchLed = tm1638_ns.class_("TM1638SwitchLed", switch.Switch, cg.Component)

View file

@ -45,11 +45,8 @@ void UponorSmatrixComponent::loop() {
// Read incoming data
while (this->available()) {
// The controller polls devices every 10 seconds, with around 200 ms between devices.
// Remember timestamps so we can send our own packets when the bus is expected to be silent.
if (now - this->last_rx_ > 500) {
this->last_poll_start_ = now;
}
// The controller polls devices every 10 seconds in some units or continuously in others with around 200 ms between
// devices. Remember timestamps so we can send our own packets when the bus is expected to be silent.
this->last_rx_ = now;
uint8_t byte;
@ -60,7 +57,8 @@ void UponorSmatrixComponent::loop() {
}
// Send packets during bus silence
if ((now - this->last_rx_ > 300) && (now - this->last_poll_start_ < 9500) && (now - this->last_tx_ > 200)) {
if (this->rx_buffer_.empty() && (now - this->last_rx_ > 50) && (now - this->last_rx_ < 100) &&
(now - this->last_tx_ > 200)) {
#ifdef USE_TIME
// Only build time packet when bus is silent and queue is empty to make sure we can send it right away
if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_())

View file

@ -93,7 +93,6 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
std::queue<std::vector<uint8_t>> tx_queue_;
uint32_t last_rx_;
uint32_t last_tx_;
uint32_t last_poll_start_;
#ifdef USE_TIME
time::RealTimeClock *time_id_{nullptr};

View file

@ -755,7 +755,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
message = std::move(arg.value);
}
}
if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") {
if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") {
// Don't change state here since either the "tts-end" or "run-end" events will do it.
return;
} else if (code == "wake-provider-missing" || code == "wake-engine-missing") {

View file

@ -77,6 +77,18 @@ struct Timer {
}
};
struct WakeWord {
std::string id;
std::string wake_word;
std::vector<std::string> trained_languages;
};
struct Configuration {
std::vector<WakeWord> available_wake_words;
std::vector<std::string> active_wake_words;
uint32_t max_active_wake_words;
};
class VoiceAssistant : public Component {
public:
void setup() override;
@ -133,6 +145,8 @@ class VoiceAssistant : public Component {
void on_audio(const api::VoiceAssistantAudio &msg);
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg);
void on_announce(const api::VoiceAssistantAnnounceRequest &msg);
void on_set_configuration(const std::vector<std::string> &active_wake_words){};
const Configuration &get_configuration() { return this->config_; };
bool is_running() const { return this->state_ != State::IDLE; }
void set_continuous(bool continuous) { this->continuous_ = continuous; }
@ -279,6 +293,8 @@ class VoiceAssistant : public Component {
AudioMode audio_mode_{AUDIO_MODE_UDP};
bool udp_socket_running_{false};
bool start_udp_socket_();
Configuration config_{};
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<VoiceAssistant> {

View file

@ -85,7 +85,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
if (!this->has_sta())
return {};
return {WiFi.localIP()};
network::IPAddresses addresses;
addresses[0] = WiFi.localIP();
#if USE_NETWORK_IPV6
int i = 1;
auto v6_addresses = WiFi.allLocalIPv6();
for (auto address : v6_addresses) {
addresses[i++] = network::IPAddress(address.toString().c_str());
}
#endif /* USE_NETWORK_IPV6 */
return addresses;
}
bool WiFiComponent::wifi_apply_hostname_() {
@ -321,6 +330,11 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
s_sta_connecting = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
// auto it = info.got_ip.ip_info;
ESP_LOGV(TAG, "Event: Got IPv6");
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
ESP_LOGV(TAG, "Event: Lost IP");
break;

View file

@ -2052,6 +2052,7 @@ def require_framework_version(
esp32_arduino=None,
esp8266_arduino=None,
rp2040_arduino=None,
bk72xx_libretiny=None,
host=None,
max_version=False,
extra_message=None,
@ -2066,6 +2067,13 @@ def require_framework_version(
msg += f". {extra_message}"
raise Invalid(msg)
required = esp_idf
elif CORE.is_bk72xx and framework == "arduino":
if bk72xx_libretiny is None:
msg = "This feature is incompatible with BK72XX"
if extra_message:
msg += f". {extra_message}"
raise Invalid(msg)
required = bk72xx_libretiny
elif CORE.is_esp32 and framework == "arduino":
if esp32_arduino is None:
msg = "This feature is incompatible with ESP32 using arduino framework"

View file

@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2024.9.0-dev"
__version__ = "2024.10.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@ -854,6 +854,7 @@ CONF_TEMPERATURE_STEP = "temperature_step"
CONF_TEXT = "text"
CONF_TEXT_SENSORS = "text_sensors"
CONF_THEN = "then"
CONF_THERMOCOUPLE_TYPE = "thermocouple_type"
CONF_THRESHOLD = "threshold"
CONF_THROTTLE = "throttle"
CONF_TILT = "tilt"

View file

@ -101,9 +101,6 @@ def valid_include(value):
def valid_project_name(value: str):
if value.count(".") != 1:
raise cv.Invalid("project name needs to have a namespace")
value = value.replace(" ", "_")
return value

View file

@ -119,7 +119,7 @@ lib_deps =
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in)
${common:arduino.lib_deps}
esphome/AsyncTCP-esphome@2.1.3 ; async_tcp
esphome/AsyncTCP-esphome@2.1.4 ; async_tcp
WiFiClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in)
@ -139,7 +139,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
extends = common:idf
platform = platformio/espressif32@5.4.0
platform_packages =
platformio/framework-espidf@~3.40407.0
platformio/framework-espidf@~3.40408.0
framework = espidf
lib_deps =

View file

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

View file

@ -10,6 +10,7 @@ sensor:
- platform: bl0942
address: 0
line_frequency: 50Hz
reset: false
voltage:
name: BL0942 Voltage
current:

View file

@ -8,6 +8,7 @@ uart:
sensor:
- platform: bl0942
reset: true
voltage:
name: BL0942 Voltage
current:

View file

@ -1,12 +1,32 @@
lvgl:
log_level: TRACE
bg_color: light_blue
disp_bg_color: 0xffff00
disp_bg_color: color_id
disp_bg_image: cat_image
theme:
obj:
border_width: 1
gradients:
- id: color_bar
direction: hor
dither: err_diff
stops:
- color: 0xFF0000
position: 0
- color: 0xFFFF00
position: 42
- color: 0x00FF00
position: 84
- color: 0x00FFFF
position: 127
- color: 0x0000FF
position: 169
- color: 0xFF00FF
position: 212
- color: 0xFF0000
position: 255
style_definitions:
- id: style_test
bg_color: 0x2F8CD8
@ -31,7 +51,7 @@ lvgl:
- id: date_style
text_font: roboto10
align: center
text_color: 0x000000
text_color: color_id2
bg_opa: cover
radius: 4
pad_all: 2
@ -386,6 +406,22 @@ lvgl:
- id: page2
widgets:
- slider:
min_value: 0
max_value: 255
bg_opa: cover
bg_grad: color_bar
radius: 0
indicator:
bg_opa: transp
knob:
radius: 1
width: 4
height: 10%
bg_color: 0x000000
width: 100%
height: 10%
align: top_mid
- button:
styles: spin_button
id: spin_up
@ -586,3 +622,13 @@ image:
color:
- id: light_blue
hex: "3340FF"
- id: color_id
red: 0.5
green: 0.5
blue: 0.5
white: 0.5
- id: color_id2
red_int: 0xFF
green_int: 123
blue_int: 64
white_int: 255

View file

@ -10,3 +10,4 @@ sensor:
cs_pin: 12
update_interval: 15s
mains_filter: 50Hz
thermocouple_type: N

View file

@ -10,3 +10,5 @@ sensor:
cs_pin: 8
update_interval: 15s
mains_filter: 50Hz
thermocouple_type: N

View file

@ -10,3 +10,5 @@ sensor:
cs_pin: 8
update_interval: 15s
mains_filter: 50Hz
thermocouple_type: N

View file

@ -10,3 +10,4 @@ sensor:
cs_pin: 12
update_interval: 15s
mains_filter: 50Hz
thermocouple_type: N

View file

@ -10,3 +10,5 @@ sensor:
cs_pin: 15
update_interval: 15s
mains_filter: 50Hz
thermocouple_type: N

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