mirror of
https://github.com/esphome/esphome.git
synced 2025-03-01 08:18:16 +00:00
Merge branch 'dev' into dev
This commit is contained in:
commit
621dd1576f
91
.github/workflows/codeql.yml
vendored
Normal file
91
.github/workflows/codeql.yml
vendored
Normal 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}}"
|
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
python ./script/sync-device_class.py
|
python ./script/sync-device_class.py
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: peter-evans/create-pull-request@v7.0.0
|
uses: peter-evans/create-pull-request@v7.0.3
|
||||||
with:
|
with:
|
||||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||||
committer: esphomebot <esphome@nabucasa.com>
|
committer: esphomebot <esphome@nabucasa.com>
|
||||||
|
@ -290,6 +290,7 @@ esphome/components/noblex/* @AGalfra
|
|||||||
esphome/components/number/* @esphome/core
|
esphome/components/number/* @esphome/core
|
||||||
esphome/components/one_wire/* @ssieb
|
esphome/components/one_wire/* @ssieb
|
||||||
esphome/components/online_image/* @guillempages
|
esphome/components/online_image/* @guillempages
|
||||||
|
esphome/components/opentherm/* @olegtarasov
|
||||||
esphome/components/ota/* @esphome/core
|
esphome/components/ota/* @esphome/core
|
||||||
esphome/components/output/* @esphome/core
|
esphome/components/output/* @esphome/core
|
||||||
esphome/components/pca6416a/* @Mat931
|
esphome/components/pca6416a/* @Mat931
|
||||||
|
@ -33,7 +33,7 @@ RUN \
|
|||||||
python3-venv=3.11.2-1+b1 \
|
python3-venv=3.11.2-1+b1 \
|
||||||
python3-wheel=0.38.4-2 \
|
python3-wheel=0.38.4-2 \
|
||||||
iputils-ping=3:20221126-1 \
|
iputils-ping=3:20221126-1 \
|
||||||
git=1:2.39.2-1.1 \
|
git=1:2.39.5-0+deb12u1 \
|
||||||
curl=7.88.1-10+deb12u7 \
|
curl=7.88.1-10+deb12u7 \
|
||||||
openssh-client=1:9.2p1-2+deb12u3 \
|
openssh-client=1:9.2p1-2+deb12u3 \
|
||||||
python3-cffi=1.15.1-5 \
|
python3-cffi=1.15.1-5 \
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from esphome import automation, core
|
from esphome import automation, core
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import font
|
from esphome.components import font
|
||||||
import esphome.components.image as espImage
|
import esphome.components.image as espImage
|
||||||
from esphome.components.image import (
|
from esphome.components.image import (
|
||||||
CONF_USE_TRANSPARENCY,
|
CONF_USE_TRANSPARENCY,
|
||||||
LOCAL_SCHEMA,
|
LOCAL_SCHEMA,
|
||||||
WEB_SCHEMA,
|
|
||||||
SOURCE_WEB,
|
|
||||||
SOURCE_LOCAL,
|
SOURCE_LOCAL,
|
||||||
|
SOURCE_WEB,
|
||||||
|
WEB_SCHEMA,
|
||||||
)
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_FILE,
|
CONF_FILE,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
|
CONF_PATH,
|
||||||
CONF_RAW_DATA_ID,
|
CONF_RAW_DATA_ID,
|
||||||
CONF_REPEAT,
|
CONF_REPEAT,
|
||||||
CONF_RESIZE,
|
CONF_RESIZE,
|
||||||
CONF_TYPE,
|
|
||||||
CONF_SOURCE,
|
CONF_SOURCE,
|
||||||
CONF_PATH,
|
CONF_TYPE,
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt
|
from esphome.core import CORE, HexInt
|
||||||
@ -172,6 +172,9 @@ async def to_code(config):
|
|||||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||||
path = espImage.compute_local_image_path(conf_file).as_posix()
|
path = espImage.compute_local_image_path(conf_file).as_posix()
|
||||||
|
else:
|
||||||
|
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = Image.open(path)
|
image = Image.open(path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -183,13 +186,12 @@ async def to_code(config):
|
|||||||
new_width_max, new_height_max = config[CONF_RESIZE]
|
new_width_max, new_height_max = config[CONF_RESIZE]
|
||||||
ratio = min(new_width_max / width, new_height_max / height)
|
ratio = min(new_width_max / width, new_height_max / height)
|
||||||
width, height = int(width * ratio), int(height * ratio)
|
width, height = int(width * ratio), int(height * ratio)
|
||||||
else:
|
elif width > 500 or height > 500:
|
||||||
if width > 500 or height > 500:
|
_LOGGER.warning(
|
||||||
_LOGGER.warning(
|
'The image "%s" you requested is very big. Please consider'
|
||||||
'The image "%s" you requested is very big. Please consider'
|
" using the resize parameter.",
|
||||||
" using the resize parameter.",
|
path,
|
||||||
path,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
transparent = config[CONF_USE_TRANSPARENCY]
|
transparent = config[CONF_USE_TRANSPARENCY]
|
||||||
|
|
||||||
@ -306,6 +308,8 @@ async def to_code(config):
|
|||||||
if transparent:
|
if transparent:
|
||||||
alpha = image.split()[-1]
|
alpha = image.split()[-1]
|
||||||
has_alpha = alpha.getextrema()[0] < 0xFF
|
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||||
|
else:
|
||||||
|
has_alpha = False
|
||||||
frame = image.convert("1", dither=Image.Dither.NONE)
|
frame = image.convert("1", dither=Image.Dither.NONE)
|
||||||
if CONF_RESIZE in config:
|
if CONF_RESIZE in config:
|
||||||
frame = frame.resize([width, height])
|
frame = frame.resize([width, height])
|
||||||
|
@ -62,6 +62,8 @@ service APIConnection {
|
|||||||
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
||||||
|
|
||||||
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) 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) {}
|
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
|
||||||
}
|
}
|
||||||
@ -1118,6 +1120,7 @@ message MediaPlayerSupportedFormat {
|
|||||||
uint32 sample_rate = 2;
|
uint32 sample_rate = 2;
|
||||||
uint32 num_channels = 3;
|
uint32 num_channels = 3;
|
||||||
MediaPlayerFormatPurpose purpose = 4;
|
MediaPlayerFormatPurpose purpose = 4;
|
||||||
|
uint32 sample_bytes = 5;
|
||||||
}
|
}
|
||||||
message ListEntitiesMediaPlayerResponse {
|
message ListEntitiesMediaPlayerResponse {
|
||||||
option (id) = 63;
|
option (id) = 63;
|
||||||
@ -1570,6 +1573,36 @@ message VoiceAssistantAnnounceFinished {
|
|||||||
bool success = 1;
|
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 ====================
|
// ==================== ALARM CONTROL PANEL ====================
|
||||||
enum AlarmControlPanelState {
|
enum AlarmControlPanelState {
|
||||||
ALARM_STATE_DISARMED = 0;
|
ALARM_STATE_DISARMED = 0;
|
||||||
|
@ -1032,6 +1032,7 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
|
|||||||
media_format.sample_rate = supported_format.sample_rate;
|
media_format.sample_rate = supported_format.sample_rate;
|
||||||
media_format.num_channels = supported_format.num_channels;
|
media_format.num_channels = supported_format.num_channels;
|
||||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||||
|
media_format.sample_bytes = supported_format.sample_bytes;
|
||||||
msg.supported_formats.push_back(media_format);
|
msg.supported_formats.push_back(media_format);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1223,6 +1224,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
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
|
@ -152,6 +152,9 @@ class APIConnection : public APIServerConnection {
|
|||||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &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
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
|
@ -5149,6 +5149,10 @@ bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt va
|
|||||||
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
|
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
this->sample_bytes = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -5168,6 +5172,7 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_uint32(2, this->sample_rate);
|
buffer.encode_uint32(2, this->sample_rate);
|
||||||
buffer.encode_uint32(3, this->num_channels);
|
buffer.encode_uint32(3, this->num_channels);
|
||||||
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
|
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
|
||||||
|
buffer.encode_uint32(5, this->sample_bytes);
|
||||||
}
|
}
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
|
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
|
||||||
@ -5190,6 +5195,11 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
|
|||||||
out.append(" purpose: ");
|
out.append(" purpose: ");
|
||||||
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
|
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" sample_bytes: ");
|
||||||
|
sprintf(buffer, "%" PRIu32, this->sample_bytes);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -7114,6 +7124,140 @@ void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
|
|||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#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) {
|
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 6: {
|
case 6: {
|
||||||
|
@ -1277,6 +1277,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
|
|||||||
uint32_t sample_rate{0};
|
uint32_t sample_rate{0};
|
||||||
uint32_t num_channels{0};
|
uint32_t num_channels{0};
|
||||||
enums::MediaPlayerFormatPurpose purpose{};
|
enums::MediaPlayerFormatPurpose purpose{};
|
||||||
|
uint32_t sample_bytes{0};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
@ -1848,6 +1849,53 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
|
|||||||
protected:
|
protected:
|
||||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||||
};
|
};
|
||||||
|
class VoiceAssistantWakeWord : public ProtoMessage {
|
||||||
|
public:
|
||||||
|
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 {
|
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
std::string object_id{};
|
std::string object_id{};
|
||||||
|
@ -496,6 +496,19 @@ bool APIServerConnectionBase::send_voice_assistant_announce_finished(const Voice
|
|||||||
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
|
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
bool APIServerConnectionBase::send_voice_assistant_configuration_response(
|
||||||
|
const VoiceAssistantConfigurationResponse &msg) {
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
return this->send_message_<VoiceAssistantConfigurationResponse>(msg, 122);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
#endif
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
|
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
|
||||||
const ListEntitiesAlarmControlPanelResponse &msg) {
|
const ListEntitiesAlarmControlPanelResponse &msg) {
|
||||||
@ -1156,6 +1169,28 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
|
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
|
||||||
#endif
|
#endif
|
||||||
this->on_voice_assistant_announce_request(msg);
|
this->on_voice_assistant_announce_request(msg);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 121: {
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
VoiceAssistantConfigurationRequest msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
this->on_voice_assistant_configuration_request(msg);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 123: {
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
VoiceAssistantSetConfiguration msg;
|
||||||
|
msg.decode(msg_data, msg_size);
|
||||||
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
|
||||||
|
#endif
|
||||||
|
this->on_voice_assistant_set_configuration(msg);
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1646,6 +1681,35 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
|
|||||||
this->subscribe_voice_assistant(msg);
|
this->subscribe_voice_assistant(msg);
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||||
if (!this->is_connection_setup()) {
|
if (!this->is_connection_setup()) {
|
||||||
|
@ -253,6 +253,15 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
|
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
|
virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
|
||||||
|
#endif
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
|
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
|
||||||
#endif
|
#endif
|
||||||
@ -425,6 +434,13 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
||||||
#endif
|
#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
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
|
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
@ -526,6 +542,12 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||||
#endif
|
#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
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
|
cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
|
||||||
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
|
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
|
||||||
cv.Optional(CONF_IBEACON_MINOR): 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_TIMEOUT, default="5min"): cv.positive_time_period,
|
||||||
cv.Optional(CONF_MIN_RSSI): cv.All(
|
cv.Optional(CONF_MIN_RSSI): cv.All(
|
||||||
cv.decibel, cv.int_range(min=-100, max=-30)
|
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))
|
cg.add(var.set_service_uuid128(uuid128))
|
||||||
|
|
||||||
if ibeacon_uuid := config.get(CONF_IBEACON_UUID):
|
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))
|
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
|
||||||
|
|
||||||
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:
|
if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:
|
||||||
|
@ -239,7 +239,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
|
|||||||
# The default/recommended esp-idf framework version
|
# The default/recommended esp-idf framework version
|
||||||
# - https://github.com/espressif/esp-idf/releases
|
# - https://github.com/espressif/esp-idf/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
|
# - 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
|
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||||
# - https://github.com/platformio/platform-espressif32/releases
|
# - https://github.com/platformio/platform-espressif32/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
||||||
|
@ -31,6 +31,13 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
|
|||||||
memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128);
|
memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128);
|
||||||
return ret;
|
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 ESPBTUUID::from_raw(const std::string &data) {
|
||||||
ESPBTUUID ret;
|
ESPBTUUID ret;
|
||||||
if (data.length() == 4) {
|
if (data.length() == 4) {
|
||||||
|
@ -20,6 +20,7 @@ class ESPBTUUID {
|
|||||||
static ESPBTUUID from_uint32(uint32_t uuid);
|
static ESPBTUUID from_uint32(uint32_t uuid);
|
||||||
|
|
||||||
static ESPBTUUID from_raw(const uint8_t *data);
|
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);
|
static ESPBTUUID from_raw(const std::string &data);
|
||||||
|
|
||||||
|
@ -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());
|
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
|
||||||
}
|
}
|
||||||
for (auto &data : this->manufacturer_datas_) {
|
for (auto &data : this->manufacturer_datas_) {
|
||||||
ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str());
|
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
|
||||||
if (this->get_ibeacon().has_value()) {
|
if (ibeacon.has_value()) {
|
||||||
auto ibeacon = this->get_ibeacon().value();
|
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
|
||||||
ESP_LOGVV(TAG, " iBeacon data:");
|
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str());
|
||||||
ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str());
|
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
|
||||||
ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major());
|
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
|
||||||
ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor());
|
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
|
||||||
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.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_) {
|
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, " 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
|
#endif
|
||||||
}
|
}
|
||||||
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
||||||
|
@ -44,10 +44,10 @@ class ESPBLEiBeacon {
|
|||||||
ESPBLEiBeacon(const uint8_t *data);
|
ESPBLEiBeacon(const uint8_t *data);
|
||||||
static optional<ESPBLEiBeacon> from_manufacturer_data(const ServiceData &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_major() { return byteswap(this->beacon_data_.major); }
|
||||||
uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); }
|
uint16_t get_minor() { return byteswap(this->beacon_data_.minor); }
|
||||||
int8_t get_signal_power() { return this->beacon_data_.signal_power; }
|
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:
|
protected:
|
||||||
struct {
|
struct {
|
||||||
|
@ -140,6 +140,8 @@ CONF_TEST_PATTERN = "test_pattern"
|
|||||||
# framerates
|
# framerates
|
||||||
CONF_MAX_FRAMERATE = "max_framerate"
|
CONF_MAX_FRAMERATE = "max_framerate"
|
||||||
CONF_IDLE_FRAMERATE = "idle_framerate"
|
CONF_IDLE_FRAMERATE = "idle_framerate"
|
||||||
|
# frame buffer
|
||||||
|
CONF_FRAME_BUFFER_COUNT = "frame_buffer_count"
|
||||||
|
|
||||||
# stream trigger
|
# stream trigger
|
||||||
CONF_ON_STREAM_START = "on_stream_start"
|
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.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
|
||||||
cv.framerate, cv.Range(min=0, max=1)
|
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.Optional(CONF_ON_STREAM_START): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
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))
|
cg.add(var.set_idle_update_interval(0))
|
||||||
else:
|
else:
|
||||||
cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE]))
|
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(var.set_frame_size(config[CONF_RESOLUTION]))
|
||||||
|
|
||||||
cg.add_define("USE_ESP32_CAMERA")
|
cg.add_define("USE_ESP32_CAMERA")
|
||||||
|
@ -127,7 +127,7 @@ void ESP32Camera::dump_config() {
|
|||||||
sensor_t *s = esp_camera_sensor_get();
|
sensor_t *s = esp_camera_sensor_get();
|
||||||
auto st = s->status;
|
auto st = s->status;
|
||||||
ESP_LOGCONFIG(TAG, " JPEG Quality: %u", st.quality);
|
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, " Contrast: %d", st.contrast);
|
||||||
ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness);
|
ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness);
|
||||||
ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation);
|
ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation);
|
||||||
@ -212,6 +212,8 @@ ESP32Camera::ESP32Camera() {
|
|||||||
this->config_.frame_size = FRAMESIZE_VGA; // 640x480
|
this->config_.frame_size = FRAMESIZE_VGA; // 640x480
|
||||||
this->config_.jpeg_quality = 10;
|
this->config_.jpeg_quality = 10;
|
||||||
this->config_.fb_count = 1;
|
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;
|
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) {
|
void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
|
||||||
this->idle_update_interval_ = 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) ---------------- */
|
/* ---------------- public API (specific) ---------------- */
|
||||||
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) {
|
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) {
|
||||||
|
@ -145,6 +145,9 @@ class ESP32Camera : public Component, public EntityBase {
|
|||||||
/* -- framerates */
|
/* -- framerates */
|
||||||
void set_max_update_interval(uint32_t max_update_interval);
|
void set_max_update_interval(uint32_t max_update_interval);
|
||||||
void set_idle_update_interval(uint32_t idle_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) */
|
/* public API (derivated) */
|
||||||
void setup() override;
|
void setup() override;
|
||||||
|
@ -30,6 +30,10 @@ CONF_I2S_MODE = "i2s_mode"
|
|||||||
CONF_PRIMARY = "primary"
|
CONF_PRIMARY = "primary"
|
||||||
CONF_SECONDARY = "secondary"
|
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_LEFT = "left"
|
||||||
CONF_RIGHT = "right"
|
CONF_RIGHT = "right"
|
||||||
CONF_STEREO = "stereo"
|
CONF_STEREO = "stereo"
|
||||||
@ -58,6 +62,7 @@ I2S_PORTS = {
|
|||||||
|
|
||||||
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
|
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
|
||||||
I2S_CHANNELS = {
|
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_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||||
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
|
CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
|
||||||
CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT,
|
CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT,
|
||||||
@ -67,17 +72,25 @@ i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t")
|
|||||||
I2S_BITS_PER_SAMPLE = {
|
I2S_BITS_PER_SAMPLE = {
|
||||||
8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT,
|
8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT,
|
||||||
16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT,
|
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,
|
32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT,
|
||||||
}
|
}
|
||||||
|
|
||||||
INTERNAL_ADC_VARIANTS = [VARIANT_ESP32]
|
i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t")
|
||||||
PDM_VARIANTS = [VARIANT_ESP32, VARIANT_ESP32S3]
|
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")
|
_validate_bits = cv.float_with_unit("bits", "bit")
|
||||||
|
|
||||||
|
|
||||||
def i2s_audio_component_schema(
|
def i2s_audio_component_schema(
|
||||||
class_: MockObjClass,
|
class_: MockObjClass,
|
||||||
|
*,
|
||||||
default_sample_rate: int,
|
default_sample_rate: int,
|
||||||
default_channel: str,
|
default_channel: str,
|
||||||
default_bits_per_sample: str,
|
default_bits_per_sample: str,
|
||||||
@ -96,6 +109,11 @@ def i2s_audio_component_schema(
|
|||||||
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
|
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum(
|
||||||
I2S_MODE_OPTIONS, lower=True
|
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),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -107,6 +125,8 @@ async def register_i2s_audio_component(var, config):
|
|||||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||||
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
|
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_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(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
@ -17,12 +17,16 @@ class I2SAudioBase : public Parented<I2SAudioComponent> {
|
|||||||
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
|
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_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_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; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
i2s_mode_t i2s_mode_{};
|
i2s_mode_t i2s_mode_{};
|
||||||
i2s_channel_fmt_t channel_;
|
i2s_channel_fmt_t channel_;
|
||||||
uint32_t sample_rate_;
|
uint32_t sample_rate_;
|
||||||
i2s_bits_per_sample_t bits_per_sample_;
|
i2s_bits_per_sample_t bits_per_sample_;
|
||||||
|
i2s_bits_per_chan_t bits_per_channel_;
|
||||||
|
bool use_apll_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class I2SAudioIn : public I2SAudioBase {};
|
class I2SAudioIn : public I2SAudioBase {};
|
||||||
|
@ -12,6 +12,10 @@ from .. import (
|
|||||||
I2SAudioOut,
|
I2SAudioOut,
|
||||||
CONF_I2S_AUDIO_ID,
|
CONF_I2S_AUDIO_ID,
|
||||||
CONF_I2S_DOUT_PIN,
|
CONF_I2S_DOUT_PIN,
|
||||||
|
CONF_LEFT,
|
||||||
|
CONF_RIGHT,
|
||||||
|
CONF_MONO,
|
||||||
|
CONF_STEREO,
|
||||||
)
|
)
|
||||||
|
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
@ -30,12 +34,12 @@ CONF_DAC_TYPE = "dac_type"
|
|||||||
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
|
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
|
||||||
|
|
||||||
INTERNAL_DAC_OPTIONS = {
|
INTERNAL_DAC_OPTIONS = {
|
||||||
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
|
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
|
||||||
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
|
CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
|
||||||
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_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]
|
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ enum I2SState : uint8_t {
|
|||||||
I2S_STATE_STOPPING,
|
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:
|
public:
|
||||||
void setup() override;
|
void setup() override;
|
||||||
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
|
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
|
||||||
|
@ -8,8 +8,6 @@ from esphome.const import CONF_ID, CONF_NUMBER
|
|||||||
from .. import (
|
from .. import (
|
||||||
CONF_I2S_DIN_PIN,
|
CONF_I2S_DIN_PIN,
|
||||||
CONF_RIGHT,
|
CONF_RIGHT,
|
||||||
INTERNAL_ADC_VARIANTS,
|
|
||||||
PDM_VARIANTS,
|
|
||||||
I2SAudioIn,
|
I2SAudioIn,
|
||||||
i2s_audio_component_schema,
|
i2s_audio_component_schema,
|
||||||
i2s_audio_ns,
|
i2s_audio_ns,
|
||||||
@ -23,12 +21,13 @@ CONF_ADC_PIN = "adc_pin"
|
|||||||
CONF_ADC_TYPE = "adc_type"
|
CONF_ADC_TYPE = "adc_type"
|
||||||
CONF_PDM = "pdm"
|
CONF_PDM = "pdm"
|
||||||
|
|
||||||
CONF_USE_APLL = "use_apll"
|
|
||||||
|
|
||||||
I2SAudioMicrophone = i2s_audio_ns.class_(
|
I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||||
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
||||||
)
|
)
|
||||||
|
|
||||||
|
INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
|
||||||
|
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
|
||||||
|
|
||||||
|
|
||||||
def validate_esp32_variant(config):
|
def validate_esp32_variant(config):
|
||||||
variant = esp32.get_esp32_variant()
|
variant = esp32.get_esp32_variant()
|
||||||
@ -45,9 +44,15 @@ def validate_esp32_variant(config):
|
|||||||
|
|
||||||
|
|
||||||
BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
||||||
i2s_audio_component_schema(I2SAudioMicrophone, 16000, CONF_RIGHT, "32bit")
|
i2s_audio_component_schema(
|
||||||
|
I2SAudioMicrophone,
|
||||||
|
default_sample_rate=16000,
|
||||||
|
default_channel=CONF_RIGHT,
|
||||||
|
default_bits_per_sample="32bit",
|
||||||
|
)
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.typed_schema(
|
cv.typed_schema(
|
||||||
{
|
{
|
||||||
@ -59,8 +64,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
"external": BASE_SCHEMA.extend(
|
"external": BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number,
|
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,
|
||||||
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -84,4 +88,3 @@ async def to_code(config):
|
|||||||
else:
|
else:
|
||||||
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
|
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
|
||||||
cg.add(var.set_pdm(config[CONF_PDM]))
|
cg.add(var.set_pdm(config[CONF_PDM]))
|
||||||
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
|
|
||||||
|
@ -58,7 +58,7 @@ void I2SAudioMicrophone::start_() {
|
|||||||
.tx_desc_auto_clear = false,
|
.tx_desc_auto_clear = false,
|
||||||
.fixed_mclk = 0,
|
.fixed_mclk = 0,
|
||||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
|
.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;
|
esp_err_t err;
|
||||||
@ -167,21 +167,24 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
this->status_clear_warning();
|
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,
|
||||||
return bytes_read;
|
// and 24-bit data to 32 bits.
|
||||||
} else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) {
|
switch (this->bits_per_sample_) {
|
||||||
std::vector<int16_t> samples;
|
case I2S_BITS_PER_SAMPLE_8BIT:
|
||||||
size_t samples_read = bytes_read / sizeof(int32_t);
|
case I2S_BITS_PER_SAMPLE_16BIT:
|
||||||
samples.resize(samples_read);
|
return bytes_read;
|
||||||
for (size_t i = 0; i < samples_read; i++) {
|
case I2S_BITS_PER_SAMPLE_24BIT:
|
||||||
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
|
case I2S_BITS_PER_SAMPLE_32BIT: {
|
||||||
samples[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
|
size_t samples_read = bytes_read / sizeof(int32_t);
|
||||||
|
for (size_t i = 0; i < samples_read; i++) {
|
||||||
|
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
|
||||||
|
buf[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
|
||||||
|
}
|
||||||
|
return samples_read * sizeof(int16_t);
|
||||||
}
|
}
|
||||||
memcpy(buf, samples.data(), samples_read * sizeof(int16_t));
|
default:
|
||||||
return samples_read * sizeof(int16_t);
|
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
|
||||||
} else {
|
return 0;
|
||||||
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void start_();
|
void start_();
|
||||||
void stop_();
|
void stop_();
|
||||||
@ -44,8 +42,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
#endif
|
#endif
|
||||||
bool pdm_{false};
|
bool pdm_{false};
|
||||||
|
|
||||||
bool use_apll_;
|
|
||||||
|
|
||||||
HighFrequencyLoopRequester high_freq_;
|
HighFrequencyLoopRequester high_freq_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@ from esphome import pins
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32, speaker
|
from esphome.components import esp32, speaker
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
CONF_I2S_DOUT_PIN,
|
CONF_I2S_DOUT_PIN,
|
||||||
CONF_LEFT,
|
CONF_LEFT,
|
||||||
|
CONF_MONO,
|
||||||
CONF_RIGHT,
|
CONF_RIGHT,
|
||||||
CONF_STEREO,
|
CONF_STEREO,
|
||||||
I2SAudioOut,
|
I2SAudioOut,
|
||||||
@ -32,7 +33,6 @@ INTERNAL_DAC_OPTIONS = {
|
|||||||
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
|
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
|
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
|
||||||
|
|
||||||
|
|
||||||
@ -45,14 +45,33 @@ def validate_esp32_variant(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
BASE_SCHEMA = speaker.SPEAKER_SCHEMA.extend(
|
BASE_SCHEMA = (
|
||||||
i2s_audio_component_schema(I2SAudioSpeaker, 16000, "stereo", "16bit")
|
speaker.SPEAKER_SCHEMA.extend(
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
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(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.typed_schema(
|
cv.typed_schema(
|
||||||
{
|
{
|
||||||
"internal": BASE_SCHEMA,
|
"internal": BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
|
||||||
|
}
|
||||||
|
),
|
||||||
"external": BASE_SCHEMA.extend(
|
"external": BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.Required(
|
cv.Required(
|
||||||
@ -77,3 +96,4 @@ async def to_code(config):
|
|||||||
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
|
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
|
||||||
else:
|
else:
|
||||||
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
|
||||||
|
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||||
|
@ -56,6 +56,21 @@ void I2SAudioSpeaker::start_() {
|
|||||||
this->task_created_ = true;
|
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) {
|
void I2SAudioSpeaker::player_task(void *params) {
|
||||||
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
|
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
|
||||||
|
|
||||||
@ -71,12 +86,12 @@ void I2SAudioSpeaker::player_task(void *params) {
|
|||||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||||
.dma_buf_count = 8,
|
.dma_buf_count = 8,
|
||||||
.dma_buf_len = 128,
|
.dma_buf_len = 256,
|
||||||
.use_apll = false,
|
.use_apll = this_speaker->use_apll_,
|
||||||
.tx_desc_auto_clear = true,
|
.tx_desc_auto_clear = true,
|
||||||
.fixed_mclk = 0,
|
.fixed_mclk = 0,
|
||||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
|
.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 SOC_I2S_SUPPORTS_DAC
|
||||||
if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
|
if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
|
||||||
@ -114,10 +129,11 @@ void I2SAudioSpeaker::player_task(void *params) {
|
|||||||
event.type = TaskEventType::STARTED;
|
event.type = TaskEventType::STARTED;
|
||||||
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
|
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
|
||||||
|
|
||||||
int16_t buffer[BUFFER_SIZE / 2];
|
int32_t buffer[BUFFER_SIZE];
|
||||||
|
|
||||||
while (true) {
|
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
|
break; // End of audio from main thread
|
||||||
}
|
}
|
||||||
if (data_event.stop) {
|
if (data_event.stop) {
|
||||||
@ -125,17 +141,28 @@ void I2SAudioSpeaker::player_task(void *params) {
|
|||||||
xQueueReset(this_speaker->buffer_queue_); // Flush queue
|
xQueueReset(this_speaker->buffer_queue_); // Flush queue
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
size_t bytes_written;
|
|
||||||
|
|
||||||
memmove(buffer, data_event.data, data_event.len);
|
const uint8_t *data = data_event.data;
|
||||||
size_t remaining = data_event.len / 2;
|
size_t remaining = data_event.len;
|
||||||
size_t current = 0;
|
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) {
|
while (remaining != 0) {
|
||||||
uint32_t sample = (buffer[current] << 16) | (buffer[current] & 0xFFFF);
|
size_t bytes_written;
|
||||||
|
esp_err_t err =
|
||||||
esp_err_t err = i2s_write(this_speaker->parent_->get_port(), &sample, sizeof(sample), &bytes_written,
|
i2s_write(this_speaker->parent_->get_port(), data, remaining, &bytes_written, (32 / portTICK_PERIOD_MS));
|
||||||
(10 / portTICK_PERIOD_MS));
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
event = {.type = TaskEventType::WARNING, .err = err};
|
event = {.type = TaskEventType::WARNING, .err = err};
|
||||||
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
||||||
@ -143,21 +170,8 @@ void I2SAudioSpeaker::player_task(void *params) {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (bytes_written != sizeof(sample)) {
|
data += bytes_written;
|
||||||
event = {.type = TaskEventType::WARNING, .err = ESP_FAIL};
|
remaining -= bytes_written;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,13 +227,11 @@ void I2SAudioSpeaker::watch_() {
|
|||||||
case TaskEventType::STARTED:
|
case TaskEventType::STARTED:
|
||||||
ESP_LOGD(TAG, "Started I2S Audio Speaker");
|
ESP_LOGD(TAG, "Started I2S Audio Speaker");
|
||||||
this->state_ = speaker::STATE_RUNNING;
|
this->state_ = speaker::STATE_RUNNING;
|
||||||
|
this->status_clear_warning();
|
||||||
break;
|
break;
|
||||||
case TaskEventType::STOPPING:
|
case TaskEventType::STOPPING:
|
||||||
ESP_LOGD(TAG, "Stopping I2S Audio Speaker");
|
ESP_LOGD(TAG, "Stopping I2S Audio Speaker");
|
||||||
break;
|
break;
|
||||||
case TaskEventType::PLAYING:
|
|
||||||
this->status_clear_warning();
|
|
||||||
break;
|
|
||||||
case TaskEventType::STOPPED:
|
case TaskEventType::STOPPED:
|
||||||
this->state_ = speaker::STATE_STOPPED;
|
this->state_ = speaker::STATE_STOPPED;
|
||||||
vTaskDelete(this->player_task_handle_);
|
vTaskDelete(this->player_task_handle_);
|
||||||
|
@ -21,7 +21,6 @@ static const size_t BUFFER_SIZE = 1024;
|
|||||||
enum class TaskEventType : uint8_t {
|
enum class TaskEventType : uint8_t {
|
||||||
STARTING = 0,
|
STARTING = 0,
|
||||||
STARTED,
|
STARTED,
|
||||||
PLAYING,
|
|
||||||
STOPPING,
|
STOPPING,
|
||||||
STOPPED,
|
STOPPED,
|
||||||
WARNING = 255,
|
WARNING = 255,
|
||||||
@ -45,6 +44,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
|
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
|
||||||
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
|
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
|
||||||
#if SOC_I2S_SUPPORTS_DAC
|
#if SOC_I2S_SUPPORTS_DAC
|
||||||
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
|
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
|
||||||
@ -69,6 +69,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
|
|||||||
QueueHandle_t buffer_queue_;
|
QueueHandle_t buffer_queue_;
|
||||||
QueueHandle_t event_queue_;
|
QueueHandle_t event_queue_;
|
||||||
|
|
||||||
|
uint32_t timeout_{0};
|
||||||
uint8_t dout_pin_{0};
|
uint8_t dout_pin_{0};
|
||||||
bool task_created_{false};
|
bool task_created_{false};
|
||||||
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from magic import Magic
|
from magic import Magic
|
||||||
|
|
||||||
from esphome import core
|
from esphome import core, external_files
|
||||||
from esphome.components import font
|
|
||||||
from esphome import external_files
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import font
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_DITHER,
|
CONF_DITHER,
|
||||||
CONF_FILE,
|
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]):
|
def load_svg_image(file: bytes, resize: tuple[int, int]):
|
||||||
# Local import only to allow "validate_pillow_installed" to run *before* importing it
|
# 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
|
# 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
|
# to the top would force configurations not using SVG to also have it
|
||||||
# installed for no reason.
|
# installed for no reason.
|
||||||
from cairosvg import svg2png
|
from cairosvg import svg2png
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
if resize:
|
if resize:
|
||||||
req_width, req_height = resize
|
req_width, req_height = resize
|
||||||
@ -274,6 +272,9 @@ async def to_code(config):
|
|||||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||||
path = compute_local_image_path(conf_file).as_posix()
|
path = compute_local_image_path(conf_file).as_posix()
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
file_contents = f.read()
|
file_contents = f.read()
|
||||||
|
@ -32,6 +32,12 @@ void MAX31856Sensor::dump_config() {
|
|||||||
LOG_PIN(" CS Pin: ", this->cs_);
|
LOG_PIN(" CS Pin: ", this->cs_);
|
||||||
ESP_LOGCONFIG(TAG, " Mains Filter: %s",
|
ESP_LOGCONFIG(TAG, " Mains Filter: %s",
|
||||||
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
|
(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);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +135,12 @@ void MAX31856Sensor::clear_fault_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MAX31856Sensor::set_thermocouple_type_() {
|
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);
|
ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type);
|
||||||
uint8_t t = this->read_register_(MAX31856_CR1_REG);
|
uint8_t t = this->read_register_(MAX31856_CR1_REG);
|
||||||
t &= 0xF0; // mask off bottom 4 bits
|
t &= 0xF0; // mask off bottom 4 bits
|
||||||
|
@ -50,7 +50,6 @@ enum MAX31856Registers {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiple types of thermocouples supported by the chip.
|
* Multiple types of thermocouples supported by the chip.
|
||||||
* Currently only K type implemented here.
|
|
||||||
*/
|
*/
|
||||||
enum MAX31856ThermocoupleType {
|
enum MAX31856ThermocoupleType {
|
||||||
MAX31856_TCTYPE_B = 0b0000, // 0x00
|
MAX31856_TCTYPE_B = 0b0000, // 0x00
|
||||||
@ -78,11 +77,15 @@ class MAX31856Sensor : public sensor::Sensor,
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
float get_setup_priority() const 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;
|
void update() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
MAX31856ConfigFilter filter_;
|
MAX31856ConfigFilter filter_;
|
||||||
|
MAX31856ThermocoupleType thermocouple_type_;
|
||||||
|
|
||||||
uint8_t read_register_(uint8_t reg);
|
uint8_t read_register_(uint8_t reg);
|
||||||
uint32_t read_register24_(uint8_t reg);
|
uint32_t read_register24_(uint8_t reg);
|
||||||
|
@ -3,6 +3,7 @@ from esphome.components import sensor, spi
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_MAINS_FILTER,
|
CONF_MAINS_FILTER,
|
||||||
|
CONF_THERMOCOUPLE_TYPE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_CELSIUS,
|
UNIT_CELSIUS,
|
||||||
@ -18,6 +19,17 @@ FILTER = {
|
|||||||
50: MAX31865ConfigFilter.FILTER_50HZ,
|
50: MAX31865ConfigFilter.FILTER_50HZ,
|
||||||
60: MAX31865ConfigFilter.FILTER_60HZ,
|
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 = (
|
CONFIG_SCHEMA = (
|
||||||
sensor.sensor_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(cv.polling_component_schema("60s"))
|
||||||
.extend(spi.spi_device_schema())
|
.extend(spi.spi_device_schema())
|
||||||
)
|
)
|
||||||
@ -44,3 +63,4 @@ async def to_code(config):
|
|||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await spi.register_spi_device(var, config)
|
await spi.register_spi_device(var, config)
|
||||||
cg.add(var.set_filter(config[CONF_MAINS_FILTER]))
|
cg.add(var.set_filter(config[CONF_MAINS_FILTER]))
|
||||||
|
cg.add(var.set_thermocouple_type(config[CONF_THERMOCOUPLE_TYPE]))
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import i2c, sensor
|
from esphome.components import i2c, sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
|
CONF_THERMOCOUPLE_TYPE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_CELSIUS,
|
UNIT_CELSIUS,
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_THERMOCOUPLE_TYPE = "thermocouple_type"
|
|
||||||
CONF_HOT_JUNCTION = "hot_junction"
|
CONF_HOT_JUNCTION = "hot_junction"
|
||||||
CONF_COLD_JUNCTION = "cold_junction"
|
CONF_COLD_JUNCTION = "cold_junction"
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ struct MediaPlayerSupportedFormat {
|
|||||||
uint32_t sample_rate;
|
uint32_t sample_rate;
|
||||||
uint32_t num_channels;
|
uint32_t num_channels;
|
||||||
MediaPlayerFormatPurpose purpose;
|
MediaPlayerFormatPurpose purpose;
|
||||||
|
uint32_t sample_bytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MediaPlayer;
|
class MediaPlayer;
|
||||||
|
@ -1,26 +1,29 @@
|
|||||||
import binascii
|
import binascii
|
||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import modbus
|
from esphome.components import modbus
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_NAME,
|
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
|
CONF_NAME,
|
||||||
CONF_OFFSET,
|
CONF_OFFSET,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
)
|
)
|
||||||
from esphome.cpp_helpers import logging
|
from esphome.cpp_helpers import logging
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALLOW_DUPLICATE_COMMANDS,
|
CONF_ALLOW_DUPLICATE_COMMANDS,
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
CONF_BYTE_OFFSET,
|
CONF_BYTE_OFFSET,
|
||||||
CONF_COMMAND_THROTTLE,
|
CONF_COMMAND_THROTTLE,
|
||||||
CONF_OFFLINE_SKIP_UPDATES,
|
|
||||||
CONF_CUSTOM_COMMAND,
|
CONF_CUSTOM_COMMAND,
|
||||||
CONF_FORCE_NEW_RANGE,
|
CONF_FORCE_NEW_RANGE,
|
||||||
|
CONF_MAX_CMD_RETRIES,
|
||||||
CONF_MODBUS_CONTROLLER_ID,
|
CONF_MODBUS_CONTROLLER_ID,
|
||||||
|
CONF_OFFLINE_SKIP_UPDATES,
|
||||||
CONF_ON_COMMAND_SENT,
|
CONF_ON_COMMAND_SENT,
|
||||||
CONF_REGISTER_COUNT,
|
CONF_REGISTER_COUNT,
|
||||||
CONF_REGISTER_TYPE,
|
CONF_REGISTER_TYPE,
|
||||||
@ -131,6 +134,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_COMMAND_THROTTLE, default="0ms"
|
CONF_COMMAND_THROTTLE, default="0ms"
|
||||||
): cv.positive_time_period_milliseconds,
|
): 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_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_SERVER_REGISTERS,
|
CONF_SERVER_REGISTERS,
|
||||||
@ -257,6 +261,7 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
|
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_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]))
|
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
||||||
if CONF_SERVER_REGISTERS in config:
|
if CONF_SERVER_REGISTERS in config:
|
||||||
for server_register in config[CONF_SERVER_REGISTERS]:
|
for server_register in config[CONF_SERVER_REGISTERS]:
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import binary_sensor
|
from esphome.components import binary_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
|
||||||
|
|
||||||
from esphome.const import CONF_ADDRESS, CONF_ID
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
MODBUS_REGISTER_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
MODBUS_REGISTER_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
|
@ -5,6 +5,7 @@ CONF_COMMAND_THROTTLE = "command_throttle"
|
|||||||
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
|
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
|
||||||
CONF_CUSTOM_COMMAND = "custom_command"
|
CONF_CUSTOM_COMMAND = "custom_command"
|
||||||
CONF_FORCE_NEW_RANGE = "force_new_range"
|
CONF_FORCE_NEW_RANGE = "force_new_range"
|
||||||
|
CONF_MAX_CMD_RETRIES = "max_cmd_retries"
|
||||||
CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
|
CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
|
||||||
CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
|
CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
|
||||||
CONF_ON_COMMAND_SENT = "on_command_sent"
|
CONF_ON_COMMAND_SENT = "on_command_sent"
|
||||||
|
@ -18,11 +18,11 @@ void ModbusController::setup() { this->create_register_ranges_(); }
|
|||||||
bool ModbusController::send_next_command_() {
|
bool ModbusController::send_next_command_() {
|
||||||
uint32_t last_send = millis() - this->last_command_timestamp_;
|
uint32_t last_send = millis() - this->last_command_timestamp_;
|
||||||
|
|
||||||
if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) {
|
if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) {
|
||||||
auto &command = command_queue_.front();
|
auto &command = this->command_queue_.front();
|
||||||
|
|
||||||
// remove from queue if command was sent too often
|
// 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_) {
|
if (!this->module_offline_) {
|
||||||
ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
|
ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
|
||||||
|
|
||||||
@ -34,11 +34,9 @@ bool ModbusController::send_next_command_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->module_offline_ = true;
|
this->module_offline_ = true;
|
||||||
ESP_LOGD(
|
ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue",
|
||||||
TAG,
|
this->address_, command->register_address);
|
||||||
"Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue",
|
this->command_queue_.pop_front();
|
||||||
this->address_, command->register_address, command->send_countdown);
|
|
||||||
command_queue_.pop_front();
|
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
|
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
|
||||||
command->register_address, command->register_count);
|
command->register_address, command->register_count);
|
||||||
@ -50,11 +48,11 @@ bool ModbusController::send_next_command_() {
|
|||||||
|
|
||||||
// remove from queue if no handler is defined
|
// remove from queue if no handler is defined
|
||||||
if (!command->on_data_func) {
|
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
|
// Queue incoming response
|
||||||
@ -77,7 +75,7 @@ void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
|
|||||||
current_command->payload = data;
|
current_command->payload = data;
|
||||||
this->incoming_queue_.push(std::move(current_command));
|
this->incoming_queue_.push(std::move(current_command));
|
||||||
ESP_LOGV(TAG, "Modbus response queued");
|
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",
|
"payload size=%zu",
|
||||||
function_code, current_command->register_address, current_command->register_count,
|
function_code, current_command->register_address, current_command->register_count,
|
||||||
current_command->payload.size());
|
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_) {
|
if (!this->allow_duplicate_commands_) {
|
||||||
// check if this command is already qeued.
|
// check if this command is already qeued.
|
||||||
// not very effective but the queue is never really large
|
// 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)) {
|
if (item->is_equal(command)) {
|
||||||
ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u",
|
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);
|
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) {
|
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
|
// Once we get a response to the command it is removed from the queue and the next command is send
|
||||||
//
|
//
|
||||||
void ModbusController::update() {
|
void ModbusController::update() {
|
||||||
if (!command_queue_.empty()) {
|
if (!this->command_queue_.empty()) {
|
||||||
ESP_LOGV(TAG, "%zu modbus commands already in queue", command_queue_.size());
|
ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size());
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGV(TAG, "Updating modbus component");
|
ESP_LOGV(TAG, "Updating modbus component");
|
||||||
}
|
}
|
||||||
@ -346,6 +344,8 @@ size_t ModbusController::create_register_ranges_() {
|
|||||||
void ModbusController::dump_config() {
|
void ModbusController::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "ModbusController:");
|
ESP_LOGCONFIG(TAG, "ModbusController:");
|
||||||
ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
|
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
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
ESP_LOGCONFIG(TAG, "sensormap");
|
ESP_LOGCONFIG(TAG, "sensormap");
|
||||||
for (auto &it : sensorset_) {
|
for (auto &it : sensorset_) {
|
||||||
@ -560,8 +560,9 @@ bool ModbusCommandItem::send() {
|
|||||||
} else {
|
} else {
|
||||||
modbusdevice->send_raw(this->payload);
|
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);
|
this->send_count_++;
|
||||||
send_countdown--;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,7 +312,6 @@ struct RegisterRange {
|
|||||||
class ModbusCommandItem {
|
class ModbusCommandItem {
|
||||||
public:
|
public:
|
||||||
static const size_t MAX_PAYLOAD_BYTES = 240;
|
static const size_t MAX_PAYLOAD_BYTES = 240;
|
||||||
static const uint8_t MAX_SEND_REPEATS = 5;
|
|
||||||
ModbusController *modbusdevice;
|
ModbusController *modbusdevice;
|
||||||
uint16_t register_address;
|
uint16_t register_address;
|
||||||
uint16_t register_count;
|
uint16_t register_count;
|
||||||
@ -322,9 +321,9 @@ class ModbusCommandItem {
|
|||||||
on_data_func;
|
on_data_func;
|
||||||
std::vector<uint8_t> payload = {};
|
std::vector<uint8_t> payload = {};
|
||||||
bool send();
|
bool send();
|
||||||
// wrong commands (esp. custom commands) can block the send queue
|
/// Check if the command should be retried based on the max_retries parameter
|
||||||
// limit the number of repeats
|
bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
|
||||||
uint8_t send_countdown{MAX_SEND_REPEATS};
|
|
||||||
/// factory methods
|
/// factory methods
|
||||||
/** Create modbus read command
|
/** Create modbus read command
|
||||||
* Function code 02-04
|
* Function code 02-04
|
||||||
@ -413,6 +412,11 @@ class ModbusCommandItem {
|
|||||||
&&handler = nullptr);
|
&&handler = nullptr);
|
||||||
|
|
||||||
bool is_equal(const ModbusCommandItem &other);
|
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.
|
/** Modbus controller class.
|
||||||
@ -464,6 +468,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
|||||||
bool get_module_offline() { return module_offline_; }
|
bool get_module_offline() { return module_offline_; }
|
||||||
/// Set callback for commands
|
/// Set callback for commands
|
||||||
void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
|
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:
|
protected:
|
||||||
/// parse sensormap_ and create range of sequential addresses
|
/// parse sensormap_ and create range of sequential addresses
|
||||||
@ -498,6 +506,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
|
|||||||
bool module_offline_;
|
bool module_offline_;
|
||||||
/// how many updates to skip if module is offline
|
/// how many updates to skip if module is offline
|
||||||
uint16_t offline_skip_updates_;
|
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_{};
|
CallbackManager<void(int, int)> command_sent_callback_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import number
|
from esphome.components import number
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
@ -12,14 +12,13 @@ from esphome.const import (
|
|||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
MODBUS_WRITE_REGISTER_TYPE,
|
MODBUS_WRITE_REGISTER_TYPE,
|
||||||
add_modbus_base_properties,
|
SENSOR_VALUE_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
SENSOR_VALUE_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
CONF_CUSTOM_COMMAND,
|
CONF_CUSTOM_COMMAND,
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import output
|
from esphome.components import output
|
||||||
from esphome.const import (
|
import esphome.config_validation as cv
|
||||||
CONF_ADDRESS,
|
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY
|
||||||
CONF_ID,
|
|
||||||
CONF_MULTIPLY,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
modbus_controller_ns,
|
SENSOR_VALUE_TYPE,
|
||||||
modbus_calc_properties,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
SENSOR_VALUE_TYPE,
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_MODBUS_CONTROLLER_ID,
|
CONF_MODBUS_CONTROLLER_ID,
|
||||||
CONF_REGISTER_TYPE,
|
CONF_REGISTER_TYPE,
|
||||||
@ -65,6 +60,7 @@ CONFIG_SCHEMA = cv.typed_schema(
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
byte_offset, reg_count = modbus_calc_properties(config)
|
byte_offset, reg_count = modbus_calc_properties(config)
|
||||||
# Binary Output
|
# Binary Output
|
||||||
|
write_template = None
|
||||||
if config[CONF_REGISTER_TYPE] == "coil":
|
if config[CONF_REGISTER_TYPE] == "coil":
|
||||||
var = cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
@ -72,7 +68,7 @@ async def to_code(config):
|
|||||||
byte_offset,
|
byte_offset,
|
||||||
)
|
)
|
||||||
if CONF_WRITE_LAMBDA in config:
|
if CONF_WRITE_LAMBDA in config:
|
||||||
template_ = await cg.process_lambda(
|
write_template = await cg.process_lambda(
|
||||||
config[CONF_WRITE_LAMBDA],
|
config[CONF_WRITE_LAMBDA],
|
||||||
[
|
[
|
||||||
(ModbusBinaryOutput.operator("ptr"), "item"),
|
(ModbusBinaryOutput.operator("ptr"), "item"),
|
||||||
@ -92,7 +88,7 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
|
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
|
||||||
if CONF_WRITE_LAMBDA in config:
|
if CONF_WRITE_LAMBDA in config:
|
||||||
template_ = await cg.process_lambda(
|
write_template = await cg.process_lambda(
|
||||||
config[CONF_WRITE_LAMBDA],
|
config[CONF_WRITE_LAMBDA],
|
||||||
[
|
[
|
||||||
(ModbusFloatOutput.operator("ptr"), "item"),
|
(ModbusFloatOutput.operator("ptr"), "item"),
|
||||||
@ -105,5 +101,5 @@ async def to_code(config):
|
|||||||
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
|
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
|
||||||
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
|
||||||
cg.add(var.set_parent(parent))
|
cg.add(var.set_parent(parent))
|
||||||
if CONF_WRITE_LAMBDA in config:
|
if write_template:
|
||||||
cg.add(var.set_write_template(template_))
|
cg.add(var.set_write_template(write_template))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import select
|
from esphome.components import select
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
|
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor
|
from esphome.components import sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
from esphome.const import CONF_ID, CONF_ADDRESS
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
|
||||||
SensorItem,
|
|
||||||
MODBUS_REGISTER_TYPE,
|
MODBUS_REGISTER_TYPE,
|
||||||
SENSOR_VALUE_TYPE,
|
SENSOR_VALUE_TYPE,
|
||||||
|
ModbusItemBaseSchema,
|
||||||
|
SensorItem,
|
||||||
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import switch
|
from esphome.components import switch
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
|
|
||||||
from esphome.const import CONF_ID, CONF_ADDRESS
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
MODBUS_REGISTER_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
MODBUS_REGISTER_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_BITMASK,
|
CONF_BITMASK,
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import text_sensor
|
from esphome.components import text_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
|
||||||
|
|
||||||
|
|
||||||
from esphome.const import CONF_ADDRESS, CONF_ID
|
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||||
|
|
||||||
from .. import (
|
from .. import (
|
||||||
add_modbus_base_properties,
|
MODBUS_REGISTER_TYPE,
|
||||||
modbus_controller_ns,
|
|
||||||
modbus_calc_properties,
|
|
||||||
validate_modbus_register,
|
|
||||||
ModbusItemBaseSchema,
|
ModbusItemBaseSchema,
|
||||||
SensorItem,
|
SensorItem,
|
||||||
MODBUS_REGISTER_TYPE,
|
add_modbus_base_properties,
|
||||||
|
modbus_calc_properties,
|
||||||
|
modbus_controller_ns,
|
||||||
|
validate_modbus_register,
|
||||||
)
|
)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
CONF_FORCE_NEW_RANGE,
|
CONF_FORCE_NEW_RANGE,
|
||||||
CONF_MODBUS_CONTROLLER_ID,
|
CONF_MODBUS_CONTROLLER_ID,
|
||||||
|
CONF_RAW_ENCODE,
|
||||||
CONF_REGISTER_COUNT,
|
CONF_REGISTER_COUNT,
|
||||||
|
CONF_REGISTER_TYPE,
|
||||||
CONF_RESPONSE_SIZE,
|
CONF_RESPONSE_SIZE,
|
||||||
CONF_SKIP_UPDATES,
|
CONF_SKIP_UPDATES,
|
||||||
CONF_RAW_ENCODE,
|
|
||||||
CONF_REGISTER_TYPE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ["modbus_controller"]
|
DEPENDENCIES = ["modbus_controller"]
|
||||||
|
57
esphome/components/opentherm/__init__.py
Normal file
57
esphome/components/opentherm/__init__.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||||
|
|
||||||
|
CODEOWNERS = ["@olegtarasov"]
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
CONF_IN_PIN = "in_pin"
|
||||||
|
CONF_OUT_PIN = "out_pin"
|
||||||
|
CONF_CH_ENABLE = "ch_enable"
|
||||||
|
CONF_DHW_ENABLE = "dhw_enable"
|
||||||
|
CONF_COOLING_ENABLE = "cooling_enable"
|
||||||
|
CONF_OTC_ACTIVE = "otc_active"
|
||||||
|
CONF_CH2_ACTIVE = "ch2_active"
|
||||||
|
CONF_SYNC_MODE = "sync_mode"
|
||||||
|
|
||||||
|
opentherm_ns = cg.esphome_ns.namespace("opentherm")
|
||||||
|
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(OpenthermHub),
|
||||||
|
cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema,
|
||||||
|
cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_CH_ENABLE, True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DHW_ENABLE, True): cv.boolean,
|
||||||
|
cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean,
|
||||||
|
cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean,
|
||||||
|
cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean,
|
||||||
|
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config: dict[str, Any]) -> None:
|
||||||
|
# Create the hub, passing the two callbacks defined below
|
||||||
|
# Since the hub is used in the callbacks, we need to define it first
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
# Set pins
|
||||||
|
in_pin = await cg.gpio_pin_expression(config[CONF_IN_PIN])
|
||||||
|
cg.add(var.set_in_pin(in_pin))
|
||||||
|
|
||||||
|
out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
|
||||||
|
cg.add(var.set_out_pin(out_pin))
|
||||||
|
|
||||||
|
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
|
||||||
|
for key, value in config.items():
|
||||||
|
if key not in non_sensors:
|
||||||
|
cg.add(getattr(var, f"set_{key}")(value))
|
277
esphome/components/opentherm/hub.cpp
Normal file
277
esphome/components/opentherm/hub.cpp
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
#include "hub.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
static const char *const TAG = "opentherm";
|
||||||
|
|
||||||
|
OpenthermData OpenthermHub::build_request_(MessageId request_id) {
|
||||||
|
OpenthermData data;
|
||||||
|
data.type = 0;
|
||||||
|
data.id = 0;
|
||||||
|
data.valueHB = 0;
|
||||||
|
data.valueLB = 0;
|
||||||
|
|
||||||
|
// First, handle the status request. This requires special logic, because we
|
||||||
|
// wouldn't want to inadvertently disable domestic hot water, for example.
|
||||||
|
// It is also included in the macro-generated code below, but that will
|
||||||
|
// never be executed, because we short-circuit it here.
|
||||||
|
if (request_id == MessageId::STATUS) {
|
||||||
|
bool const ch_enabled = this->ch_enable;
|
||||||
|
bool dhw_enabled = this->dhw_enable;
|
||||||
|
bool cooling_enabled = this->cooling_enable;
|
||||||
|
bool otc_enabled = this->otc_active;
|
||||||
|
bool ch2_enabled = this->ch2_active;
|
||||||
|
|
||||||
|
data.type = MessageType::READ_DATA;
|
||||||
|
data.id = MessageId::STATUS;
|
||||||
|
data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4);
|
||||||
|
|
||||||
|
// Disable incomplete switch statement warnings, because the cases in each
|
||||||
|
// switch are generated based on the configured sensors and inputs.
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wswitch"
|
||||||
|
|
||||||
|
// TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on
|
||||||
|
// which sensors are enabled in config.
|
||||||
|
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return OpenthermData();
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenthermHub::OpenthermHub() : Component() {}
|
||||||
|
|
||||||
|
void OpenthermHub::process_response(OpenthermData &data) {
|
||||||
|
ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
|
||||||
|
this->opentherm_->message_id_to_str((MessageId) data.id));
|
||||||
|
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::setup() {
|
||||||
|
ESP_LOGD(TAG, "Setting up OpenTherm component");
|
||||||
|
this->opentherm_ = make_unique<OpenTherm>(this->in_pin_, this->out_pin_);
|
||||||
|
if (!this->opentherm_->initialize()) {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details.");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there is at least one request, as we are required to
|
||||||
|
// communicate at least once every second. Sending the status request is
|
||||||
|
// good practice anyway.
|
||||||
|
this->add_repeating_message(MessageId::STATUS);
|
||||||
|
|
||||||
|
this->current_message_iterator_ = this->initial_messages_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); }
|
||||||
|
|
||||||
|
void OpenthermHub::loop() {
|
||||||
|
if (this->sync_mode_) {
|
||||||
|
this->sync_loop_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cur_time = millis();
|
||||||
|
auto const cur_mode = this->opentherm_->get_mode();
|
||||||
|
switch (cur_mode) {
|
||||||
|
case OperationMode::WRITE:
|
||||||
|
case OperationMode::READ:
|
||||||
|
case OperationMode::LISTEN:
|
||||||
|
if (!this->check_timings_(cur_time)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->last_mode_ = cur_mode;
|
||||||
|
break;
|
||||||
|
case OperationMode::ERROR_PROTOCOL:
|
||||||
|
if (this->last_mode_ == OperationMode::WRITE) {
|
||||||
|
this->handle_protocol_write_error_();
|
||||||
|
} else if (this->last_mode_ == OperationMode::READ) {
|
||||||
|
this->handle_protocol_read_error_();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->stop_opentherm_();
|
||||||
|
break;
|
||||||
|
case OperationMode::ERROR_TIMEOUT:
|
||||||
|
this->handle_timeout_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
break;
|
||||||
|
case OperationMode::IDLE:
|
||||||
|
if (this->should_skip_loop_(cur_time)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->start_conversation_();
|
||||||
|
break;
|
||||||
|
case OperationMode::SENT:
|
||||||
|
// Message sent, now listen for the response.
|
||||||
|
this->opentherm_->listen();
|
||||||
|
break;
|
||||||
|
case OperationMode::RECEIVED:
|
||||||
|
this->read_response_();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::sync_loop_() {
|
||||||
|
if (!this->opentherm_->is_idle()) {
|
||||||
|
ESP_LOGE(TAG, "OpenTherm is not idle at the start of the loop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cur_time = millis();
|
||||||
|
|
||||||
|
this->check_timings_(cur_time);
|
||||||
|
|
||||||
|
if (this->should_skip_loop_(cur_time)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->start_conversation_();
|
||||||
|
|
||||||
|
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
|
||||||
|
ESP_LOGE(TAG, "Hub timeout triggered during send");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->opentherm_->is_error()) {
|
||||||
|
this->handle_protocol_write_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
} else if (!this->opentherm_->is_sent()) {
|
||||||
|
ESP_LOGW(TAG, "Unexpected state after sending request: %s",
|
||||||
|
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for the response
|
||||||
|
this->opentherm_->listen();
|
||||||
|
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
|
||||||
|
ESP_LOGE(TAG, "Hub timeout triggered during receive");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->opentherm_->is_timeout()) {
|
||||||
|
this->handle_timeout_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
} else if (this->opentherm_->is_protocol_error()) {
|
||||||
|
this->handle_protocol_read_error_();
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
} else if (!this->opentherm_->has_message()) {
|
||||||
|
ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
|
||||||
|
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->read_response_();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenthermHub::check_timings_(uint32_t cur_time) {
|
||||||
|
if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
|
||||||
|
ESP_LOGW(TAG,
|
||||||
|
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other "
|
||||||
|
"components that might slow the loop down.",
|
||||||
|
(int) (cur_time - this->last_conversation_start_));
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
|
||||||
|
if (this->last_conversation_end_ > 0 && (cur_time - this->last_conversation_end_) < 100) {
|
||||||
|
ESP_LOGV(TAG, "Less than 100 ms elapsed since last convo, skipping this iteration");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::start_conversation_() {
|
||||||
|
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) {
|
||||||
|
this->sending_initial_ = false;
|
||||||
|
this->current_message_iterator_ = this->repeating_messages_.begin();
|
||||||
|
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) {
|
||||||
|
this->current_message_iterator_ = this->repeating_messages_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto request = this->build_request_(*this->current_message_iterator_);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
|
||||||
|
this->opentherm_->message_id_to_str((MessageId) request.id));
|
||||||
|
ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str());
|
||||||
|
// Send the request
|
||||||
|
this->last_conversation_start_ = millis();
|
||||||
|
this->opentherm_->send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::read_response_() {
|
||||||
|
OpenthermData response;
|
||||||
|
if (!this->opentherm_->get_message(response)) {
|
||||||
|
ESP_LOGW(TAG, "Couldn't get the response, but flags indicated success. This is a bug.");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->stop_opentherm_();
|
||||||
|
|
||||||
|
this->process_response(response);
|
||||||
|
|
||||||
|
this->current_message_iterator_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::stop_opentherm_() {
|
||||||
|
this->opentherm_->stop();
|
||||||
|
this->last_conversation_end_ = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::handle_protocol_write_error_() {
|
||||||
|
ESP_LOGW(TAG, "Error while sending request: %s",
|
||||||
|
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
|
||||||
|
ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::handle_protocol_read_error_() {
|
||||||
|
OpenThermError error;
|
||||||
|
this->opentherm_->get_protocol_error(error);
|
||||||
|
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermHub::handle_timeout_error_() {
|
||||||
|
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
|
||||||
|
this->stop_opentherm_();
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ID(x) x
|
||||||
|
#define SHOW2(x) #x
|
||||||
|
#define SHOW(x) SHOW2(x)
|
||||||
|
|
||||||
|
void OpenthermHub::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "OpenTherm:");
|
||||||
|
LOG_PIN(" In: ", this->in_pin_);
|
||||||
|
LOG_PIN(" Out: ", this->out_pin_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Initial requests:");
|
||||||
|
for (auto type : this->initial_messages_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " - %d", type);
|
||||||
|
}
|
||||||
|
ESP_LOGCONFIG(TAG, " Repeating requests:");
|
||||||
|
for (auto type : this->repeating_messages_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " - %d", type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
110
esphome/components/opentherm/hub.h
Normal file
110
esphome/components/opentherm/hub.h
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include "opentherm.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
// OpenTherm component for ESPHome
|
||||||
|
class OpenthermHub : public Component {
|
||||||
|
protected:
|
||||||
|
// Communication pins for the OpenTherm interface
|
||||||
|
InternalGPIOPin *in_pin_, *out_pin_;
|
||||||
|
// The OpenTherm interface
|
||||||
|
std::unique_ptr<OpenTherm> opentherm_;
|
||||||
|
|
||||||
|
// The set of initial messages to send on starting communication with the boiler
|
||||||
|
std::unordered_set<MessageId> initial_messages_;
|
||||||
|
// and the repeating messages which are sent repeatedly to update various sensors
|
||||||
|
// and boiler parameters (like the setpoint).
|
||||||
|
std::unordered_set<MessageId> repeating_messages_;
|
||||||
|
// Indicates if we are still working on the initial requests or not
|
||||||
|
bool sending_initial_ = true;
|
||||||
|
// Index for the current request in one of the _requests sets.
|
||||||
|
std::unordered_set<MessageId>::const_iterator current_message_iterator_;
|
||||||
|
|
||||||
|
uint32_t last_conversation_start_ = 0;
|
||||||
|
uint32_t last_conversation_end_ = 0;
|
||||||
|
OperationMode last_mode_ = IDLE;
|
||||||
|
OpenthermData last_request_;
|
||||||
|
|
||||||
|
// Synchronous communication mode prevents other components from disabling interrupts while
|
||||||
|
// we are talking to the boiler. Enable if you experience random intermittent invalid response errors.
|
||||||
|
// Very likely to happen while using Dallas temperature sensors.
|
||||||
|
bool sync_mode_ = false;
|
||||||
|
|
||||||
|
// Create OpenTherm messages based on the message id
|
||||||
|
OpenthermData build_request_(MessageId request_id);
|
||||||
|
void handle_protocol_write_error_();
|
||||||
|
void handle_protocol_read_error_();
|
||||||
|
void handle_timeout_error_();
|
||||||
|
void stop_opentherm_();
|
||||||
|
void start_conversation_();
|
||||||
|
void read_response_();
|
||||||
|
bool check_timings_(uint32_t cur_time);
|
||||||
|
bool should_skip_loop_(uint32_t cur_time) const;
|
||||||
|
void sync_loop_();
|
||||||
|
|
||||||
|
template<typename F> bool spin_wait_(uint32_t timeout, F func) {
|
||||||
|
auto start_time = millis();
|
||||||
|
while (func()) {
|
||||||
|
yield();
|
||||||
|
auto cur_time = millis();
|
||||||
|
if (cur_time - start_time >= timeout) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor with references to the global interrupt handlers
|
||||||
|
OpenthermHub();
|
||||||
|
|
||||||
|
// Handle responses from the OpenTherm interface
|
||||||
|
void process_response(OpenthermData &data);
|
||||||
|
|
||||||
|
// Setters for the input and output OpenTherm interface pins
|
||||||
|
void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; }
|
||||||
|
void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; }
|
||||||
|
|
||||||
|
// Add a request to the set of initial requests
|
||||||
|
void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
|
||||||
|
// Add a request to the set of repeating requests. Note that a large number of repeating
|
||||||
|
// requests will slow down communication with the boiler. Each request may take up to 1 second,
|
||||||
|
// so with all sensors enabled, it may take about half a minute before a change in setpoint
|
||||||
|
// will be processed.
|
||||||
|
void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
|
||||||
|
|
||||||
|
// There are five status variables, which can either be set as a simple variable,
|
||||||
|
// or using a switch. ch_enable and dhw_enable default to true, the others to false.
|
||||||
|
bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false;
|
||||||
|
|
||||||
|
// Setters for the status variables
|
||||||
|
void set_ch_enable(bool value) { this->ch_enable = value; }
|
||||||
|
void set_dhw_enable(bool value) { this->dhw_enable = value; }
|
||||||
|
void set_cooling_enable(bool value) { this->cooling_enable = value; }
|
||||||
|
void set_otc_active(bool value) { this->otc_active = value; }
|
||||||
|
void set_ch2_active(bool value) { this->ch2_active = value; }
|
||||||
|
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
|
||||||
|
|
||||||
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void on_shutdown() override;
|
||||||
|
void loop() override;
|
||||||
|
void dump_config() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
568
esphome/components/opentherm/opentherm.cpp
Normal file
568
esphome/components/opentherm/opentherm.cpp
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
/*
|
||||||
|
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
|
||||||
|
* heavily modified to comply with ESPHome coding standards and provide better logging.
|
||||||
|
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "opentherm.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
#include "driver/timer.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#endif
|
||||||
|
#ifdef ESP8266
|
||||||
|
#include "Arduino.h"
|
||||||
|
#endif
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <bitset>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::bitset;
|
||||||
|
using std::stringstream;
|
||||||
|
using std::to_string;
|
||||||
|
|
||||||
|
static const char *const TAG = "opentherm";
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
OpenTherm *OpenTherm::instance_ = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout)
|
||||||
|
: in_pin_(in_pin),
|
||||||
|
out_pin_(out_pin),
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
timer_group_(TIMER_GROUP_0),
|
||||||
|
timer_idx_(TIMER_0),
|
||||||
|
#endif
|
||||||
|
mode_(OperationMode::IDLE),
|
||||||
|
error_type_(ProtocolErrorType::NO_ERROR),
|
||||||
|
capture_(0),
|
||||||
|
clock_(0),
|
||||||
|
data_(0),
|
||||||
|
bit_pos_(0),
|
||||||
|
timeout_counter_(-1),
|
||||||
|
device_timeout_(device_timeout) {
|
||||||
|
this->isr_in_pin_ = in_pin->to_isr();
|
||||||
|
this->isr_out_pin_ = out_pin->to_isr();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenTherm::initialize() {
|
||||||
|
#ifdef ESP8266
|
||||||
|
OpenTherm::instance_ = this;
|
||||||
|
#endif
|
||||||
|
this->in_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||||
|
this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||||
|
this->out_pin_->digital_write(true);
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
return this->init_esp32_timer_();
|
||||||
|
#else
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::listen() {
|
||||||
|
this->stop_timer_();
|
||||||
|
this->timeout_counter_ = this->device_timeout_ * 5; // timer_ ticks at 5 ticks/ms
|
||||||
|
|
||||||
|
this->mode_ = OperationMode::LISTEN;
|
||||||
|
this->data_ = 0;
|
||||||
|
this->bit_pos_ = 0;
|
||||||
|
|
||||||
|
this->start_read_timer_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::send(OpenthermData &data) {
|
||||||
|
this->stop_timer_();
|
||||||
|
this->data_ = data.type;
|
||||||
|
this->data_ = (this->data_ << 12) | data.id;
|
||||||
|
this->data_ = (this->data_ << 8) | data.valueHB;
|
||||||
|
this->data_ = (this->data_ << 8) | data.valueLB;
|
||||||
|
if (!check_parity_(this->data_)) {
|
||||||
|
this->data_ = this->data_ | 0x80000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->clock_ = 1; // clock starts at HIGH
|
||||||
|
this->bit_pos_ = 33; // count down (33 == start bit, 32-1 data, 0 == stop bit)
|
||||||
|
this->mode_ = OperationMode::WRITE;
|
||||||
|
|
||||||
|
this->start_write_timer_();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenTherm::get_message(OpenthermData &data) {
|
||||||
|
if (this->mode_ == OperationMode::RECEIVED) {
|
||||||
|
data.type = (this->data_ >> 28) & 0x7;
|
||||||
|
data.id = (this->data_ >> 16) & 0xFF;
|
||||||
|
data.valueHB = (this->data_ >> 8) & 0xFF;
|
||||||
|
data.valueLB = this->data_ & 0xFF;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenTherm::get_protocol_error(OpenThermError &error) {
|
||||||
|
if (this->mode_ != OperationMode::ERROR_PROTOCOL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.error_type = this->error_type_;
|
||||||
|
error.bit_pos = this->bit_pos_;
|
||||||
|
error.capture = this->capture_;
|
||||||
|
error.clock = this->clock_;
|
||||||
|
error.data = this->data_;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::stop() {
|
||||||
|
this->stop_timer_();
|
||||||
|
this->mode_ = OperationMode::IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::read_() {
|
||||||
|
this->data_ = 0;
|
||||||
|
this->bit_pos_ = 0;
|
||||||
|
this->mode_ = OperationMode::READ;
|
||||||
|
this->capture_ = 1; // reset counter and add as if read start bit
|
||||||
|
this->clock_ = 1; // clock is high at the start of comm
|
||||||
|
this->start_read_timer_(); // get us into 1/4 of manchester code. 5 timer ticks constitute 1 ms, which is 1 bit
|
||||||
|
// period in OpenTherm.
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
|
||||||
|
if (arg->mode_ == OperationMode::LISTEN) {
|
||||||
|
if (arg->timeout_counter_ == 0) {
|
||||||
|
arg->mode_ = OperationMode::ERROR_TIMEOUT;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool const value = arg->isr_in_pin_.digital_read();
|
||||||
|
if (value) { // incoming data (rising signal)
|
||||||
|
arg->read_();
|
||||||
|
}
|
||||||
|
if (arg->timeout_counter_ > 0) {
|
||||||
|
arg->timeout_counter_--;
|
||||||
|
}
|
||||||
|
} else if (arg->mode_ == OperationMode::READ) {
|
||||||
|
bool const value = arg->isr_in_pin_.digital_read();
|
||||||
|
uint8_t const last = (arg->capture_ & 1);
|
||||||
|
if (value != last) {
|
||||||
|
// transition of signal from last sampling
|
||||||
|
if (arg->clock_ == 1 && arg->capture_ > 0xF) {
|
||||||
|
// no transition in the middle of the bit
|
||||||
|
arg->mode_ = OperationMode::ERROR_PROTOCOL;
|
||||||
|
arg->error_type_ = ProtocolErrorType::NO_TRANSITION;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
} else if (arg->clock_ == 1 || arg->capture_ > 0xF) {
|
||||||
|
// transition in the middle of the bit OR no transition between two bit, both are valid data points
|
||||||
|
if (arg->bit_pos_ == BitPositions::STOP_BIT) {
|
||||||
|
// expecting stop bit
|
||||||
|
auto stop_bit_error = arg->verify_stop_bit_(last);
|
||||||
|
if (stop_bit_error == ProtocolErrorType::NO_ERROR) {
|
||||||
|
arg->mode_ = OperationMode::RECEIVED;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// end of data not verified, invalid data
|
||||||
|
arg->mode_ = OperationMode::ERROR_PROTOCOL;
|
||||||
|
arg->error_type_ = stop_bit_error;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// normal data point at clock high
|
||||||
|
arg->bit_read_(last);
|
||||||
|
arg->clock_ = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// clock low, not a data point, switch clock
|
||||||
|
arg->clock_ = 1;
|
||||||
|
}
|
||||||
|
arg->capture_ = 1; // reset counter
|
||||||
|
} else if (arg->capture_ > 0xFF) {
|
||||||
|
// no change for too long, invalid mancheter encoding
|
||||||
|
arg->mode_ = OperationMode::ERROR_PROTOCOL;
|
||||||
|
arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG;
|
||||||
|
arg->stop_timer_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
arg->capture_ = (arg->capture_ << 1) | value;
|
||||||
|
} else if (arg->mode_ == OperationMode::WRITE) {
|
||||||
|
// write data to pin
|
||||||
|
if (arg->bit_pos_ == 33 || arg->bit_pos_ == 0) { // start bit
|
||||||
|
arg->write_bit_(1, arg->clock_);
|
||||||
|
} else { // data bits
|
||||||
|
arg->write_bit_(read_bit(arg->data_, arg->bit_pos_ - 1), arg->clock_);
|
||||||
|
}
|
||||||
|
if (arg->clock_ == 0) {
|
||||||
|
if (arg->bit_pos_ <= 0) { // check termination
|
||||||
|
arg->mode_ = OperationMode::SENT; // all data written
|
||||||
|
arg->stop_timer_();
|
||||||
|
}
|
||||||
|
arg->bit_pos_--;
|
||||||
|
arg->clock_ = 1;
|
||||||
|
} else {
|
||||||
|
arg->clock_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance_); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) {
|
||||||
|
this->data_ = (this->data_ << 1) | value;
|
||||||
|
this->bit_pos_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) {
|
||||||
|
if (value) { // stop bit detected
|
||||||
|
return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR;
|
||||||
|
} else { // no stop bit detected, error
|
||||||
|
return ProtocolErrorType::INVALID_STOP_BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) {
|
||||||
|
if (clock == 1) { // left part of manchester encoding
|
||||||
|
this->isr_out_pin_.digital_write(!high); // low means logical 1 to protocol
|
||||||
|
} else { // right part of manchester encoding
|
||||||
|
this->isr_out_pin_.digital_write(high); // high means logical 0 to protocol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
|
||||||
|
bool OpenTherm::init_esp32_timer_() {
|
||||||
|
// Search for a free timer. Maybe unstable, we'll see.
|
||||||
|
int cur_timer = 0;
|
||||||
|
timer_group_t timer_group = TIMER_GROUP_0;
|
||||||
|
timer_idx_t timer_idx = TIMER_0;
|
||||||
|
bool timer_found = false;
|
||||||
|
|
||||||
|
for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) {
|
||||||
|
timer_config_t temp_config;
|
||||||
|
timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1;
|
||||||
|
timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2);
|
||||||
|
|
||||||
|
auto err = timer_get_config(timer_group, timer_idx, &temp_config);
|
||||||
|
if (err == ESP_ERR_INVALID_ARG) {
|
||||||
|
// Error means timer was not initialized (or other things, but we are careful with our args)
|
||||||
|
timer_found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timer_found) {
|
||||||
|
ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx);
|
||||||
|
this->timer_group_ = timer_group;
|
||||||
|
this->timer_idx_ = timer_idx;
|
||||||
|
|
||||||
|
timer_config_t const config = {
|
||||||
|
.alarm_en = TIMER_ALARM_EN,
|
||||||
|
.counter_en = TIMER_PAUSE,
|
||||||
|
.intr_type = TIMER_INTR_LEVEL,
|
||||||
|
.counter_dir = TIMER_COUNT_UP,
|
||||||
|
.auto_reload = TIMER_AUTORELOAD_EN,
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
.clk_src = TIMER_SRC_CLK_DEFAULT,
|
||||||
|
#endif
|
||||||
|
.divider = 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t result;
|
||||||
|
|
||||||
|
result = timer_init(this->timer_group_, this->timer_idx_, &config);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to init timer. Error: %s", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast<bool (*)(void *)>(timer_isr),
|
||||||
|
this, 0);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) {
|
||||||
|
esp_err_t result;
|
||||||
|
|
||||||
|
result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_start(this->timer_group_, this->timer_idx_);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 kHz timer_
|
||||||
|
void IRAM_ATTR OpenTherm::start_read_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
this->start_esp32_timer_(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 kHz timer_
|
||||||
|
void IRAM_ATTR OpenTherm::start_write_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
this->start_esp32_timer_(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IRAM_ATTR OpenTherm::stop_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
|
||||||
|
esp_err_t result;
|
||||||
|
|
||||||
|
result = timer_pause(this->timer_group_, this->timer_idx_);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
const auto *error = esp_err_to_name(result);
|
||||||
|
ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // END ESP32
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
// 5 kHz timer_
|
||||||
|
void OpenTherm::start_read_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
|
||||||
|
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max)
|
||||||
|
timer1_write(1000); // 5kHz
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 kHz timer_
|
||||||
|
void OpenTherm::start_write_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
|
||||||
|
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max)
|
||||||
|
timer1_write(2500); // 2kHz
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenTherm::stop_timer_() {
|
||||||
|
InterruptLock const lock;
|
||||||
|
timer1_disable();
|
||||||
|
timer1_detachInterrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // END ESP8266
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
|
||||||
|
bool OpenTherm::check_parity_(uint32_t val) {
|
||||||
|
val ^= val >> 16;
|
||||||
|
val ^= val >> 8;
|
||||||
|
val ^= val >> 4;
|
||||||
|
val ^= val >> 2;
|
||||||
|
val ^= val >> 1;
|
||||||
|
return (~val) & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TO_STRING_MEMBER(name) \
|
||||||
|
case name: \
|
||||||
|
return #name;
|
||||||
|
|
||||||
|
const char *OpenTherm::operation_mode_to_str(OperationMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
TO_STRING_MEMBER(IDLE)
|
||||||
|
TO_STRING_MEMBER(LISTEN)
|
||||||
|
TO_STRING_MEMBER(READ)
|
||||||
|
TO_STRING_MEMBER(RECEIVED)
|
||||||
|
TO_STRING_MEMBER(WRITE)
|
||||||
|
TO_STRING_MEMBER(SENT)
|
||||||
|
TO_STRING_MEMBER(ERROR_PROTOCOL)
|
||||||
|
TO_STRING_MEMBER(ERROR_TIMEOUT)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
|
||||||
|
switch (error_type) {
|
||||||
|
TO_STRING_MEMBER(NO_ERROR)
|
||||||
|
TO_STRING_MEMBER(NO_TRANSITION)
|
||||||
|
TO_STRING_MEMBER(INVALID_STOP_BIT)
|
||||||
|
TO_STRING_MEMBER(PARITY_ERROR)
|
||||||
|
TO_STRING_MEMBER(NO_CHANGE_TOO_LONG)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char *OpenTherm::message_type_to_str(MessageType message_type) {
|
||||||
|
switch (message_type) {
|
||||||
|
TO_STRING_MEMBER(READ_DATA)
|
||||||
|
TO_STRING_MEMBER(READ_ACK)
|
||||||
|
TO_STRING_MEMBER(WRITE_DATA)
|
||||||
|
TO_STRING_MEMBER(WRITE_ACK)
|
||||||
|
TO_STRING_MEMBER(INVALID_DATA)
|
||||||
|
TO_STRING_MEMBER(DATA_INVALID)
|
||||||
|
TO_STRING_MEMBER(UNKNOWN_DATAID)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *OpenTherm::message_id_to_str(MessageId id) {
|
||||||
|
switch (id) {
|
||||||
|
TO_STRING_MEMBER(STATUS)
|
||||||
|
TO_STRING_MEMBER(CH_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(CONTROLLER_CONFIG)
|
||||||
|
TO_STRING_MEMBER(DEVICE_CONFIG)
|
||||||
|
TO_STRING_MEMBER(COMMAND_CODE)
|
||||||
|
TO_STRING_MEMBER(FAULT_FLAGS)
|
||||||
|
TO_STRING_MEMBER(REMOTE)
|
||||||
|
TO_STRING_MEMBER(COOLING_CONTROL)
|
||||||
|
TO_STRING_MEMBER(CH2_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(CH_SETPOINT_OVERRIDE)
|
||||||
|
TO_STRING_MEMBER(TSP_COUNT)
|
||||||
|
TO_STRING_MEMBER(TSP_COMMAND)
|
||||||
|
TO_STRING_MEMBER(FHB_SIZE)
|
||||||
|
TO_STRING_MEMBER(FHB_COMMAND)
|
||||||
|
TO_STRING_MEMBER(MAX_MODULATION_LEVEL)
|
||||||
|
TO_STRING_MEMBER(MAX_BOILER_CAPACITY)
|
||||||
|
TO_STRING_MEMBER(ROOM_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(MODULATION_LEVEL)
|
||||||
|
TO_STRING_MEMBER(CH_WATER_PRESSURE)
|
||||||
|
TO_STRING_MEMBER(DHW_FLOW_RATE)
|
||||||
|
TO_STRING_MEMBER(DAY_TIME)
|
||||||
|
TO_STRING_MEMBER(DATE)
|
||||||
|
TO_STRING_MEMBER(YEAR)
|
||||||
|
TO_STRING_MEMBER(ROOM_SETPOINT_CH2)
|
||||||
|
TO_STRING_MEMBER(ROOM_TEMP)
|
||||||
|
TO_STRING_MEMBER(FEED_TEMP)
|
||||||
|
TO_STRING_MEMBER(DHW_TEMP)
|
||||||
|
TO_STRING_MEMBER(OUTSIDE_TEMP)
|
||||||
|
TO_STRING_MEMBER(RETURN_WATER_TEMP)
|
||||||
|
TO_STRING_MEMBER(SOLAR_STORE_TEMP)
|
||||||
|
TO_STRING_MEMBER(SOLAR_COLLECT_TEMP)
|
||||||
|
TO_STRING_MEMBER(FEED_TEMP_CH2)
|
||||||
|
TO_STRING_MEMBER(DHW2_TEMP)
|
||||||
|
TO_STRING_MEMBER(EXHAUST_TEMP)
|
||||||
|
TO_STRING_MEMBER(FAN_SPEED)
|
||||||
|
TO_STRING_MEMBER(FLAME_CURRENT)
|
||||||
|
TO_STRING_MEMBER(DHW_BOUNDS)
|
||||||
|
TO_STRING_MEMBER(CH_BOUNDS)
|
||||||
|
TO_STRING_MEMBER(OTC_CURVE_BOUNDS)
|
||||||
|
TO_STRING_MEMBER(DHW_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(MAX_CH_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(OTC_CURVE_RATIO)
|
||||||
|
TO_STRING_MEMBER(HVAC_STATUS)
|
||||||
|
TO_STRING_MEMBER(REL_VENT_SETPOINT)
|
||||||
|
TO_STRING_MEMBER(DEVICE_VENT)
|
||||||
|
TO_STRING_MEMBER(REL_VENTILATION)
|
||||||
|
TO_STRING_MEMBER(REL_HUMID_EXHAUST)
|
||||||
|
TO_STRING_MEMBER(SUPPLY_INLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(EXHAUST_INLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP)
|
||||||
|
TO_STRING_MEMBER(NOM_REL_VENTILATION)
|
||||||
|
TO_STRING_MEMBER(OVERRIDE_FUNC)
|
||||||
|
TO_STRING_MEMBER(OEM_DIAGNOSTIC)
|
||||||
|
TO_STRING_MEMBER(BURNER_STARTS)
|
||||||
|
TO_STRING_MEMBER(CH_PUMP_STARTS)
|
||||||
|
TO_STRING_MEMBER(DHW_PUMP_STARTS)
|
||||||
|
TO_STRING_MEMBER(DHW_BURNER_STARTS)
|
||||||
|
TO_STRING_MEMBER(BURNER_HOURS)
|
||||||
|
TO_STRING_MEMBER(CH_PUMP_HOURS)
|
||||||
|
TO_STRING_MEMBER(DHW_PUMP_HOURS)
|
||||||
|
TO_STRING_MEMBER(DHW_BURNER_HOURS)
|
||||||
|
TO_STRING_MEMBER(OT_VERSION_CONTROLLER)
|
||||||
|
TO_STRING_MEMBER(OT_VERSION_DEVICE)
|
||||||
|
TO_STRING_MEMBER(VERSION_CONTROLLER)
|
||||||
|
TO_STRING_MEMBER(VERSION_DEVICE)
|
||||||
|
default:
|
||||||
|
return "<INVALID>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string OpenTherm::debug_data(OpenthermData &data) {
|
||||||
|
stringstream result;
|
||||||
|
result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " "
|
||||||
|
<< bitset<8>(data.valueLB) << "\n";
|
||||||
|
result << "type: " << this->message_type_to_str((MessageType) data.type) << "; ";
|
||||||
|
result << "id: " << to_string(data.id) << "; ";
|
||||||
|
result << "HB: " << to_string(data.valueHB) << "; ";
|
||||||
|
result << "LB: " << to_string(data.valueLB) << "; ";
|
||||||
|
result << "uint_16: " << to_string(data.u16()) << "; ";
|
||||||
|
result << "float: " << to_string(data.f88());
|
||||||
|
|
||||||
|
return result.str();
|
||||||
|
}
|
||||||
|
std::string OpenTherm::debug_error(OpenThermError &error) {
|
||||||
|
stringstream result;
|
||||||
|
result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; ";
|
||||||
|
result << "data: ";
|
||||||
|
result << format_hex(error.data);
|
||||||
|
result << "; clock: " << to_string(clock_);
|
||||||
|
result << "; capture: " << bitset<32>(error.capture);
|
||||||
|
result << "; bit_pos: " << to_string(error.bit_pos);
|
||||||
|
|
||||||
|
return result.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
|
||||||
|
|
||||||
|
void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); }
|
||||||
|
|
||||||
|
uint16_t OpenthermData::u16() {
|
||||||
|
uint16_t const value = this->valueHB;
|
||||||
|
return (value << 8) | this->valueLB;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermData::u16(uint16_t value) {
|
||||||
|
this->valueLB = value & 0xFF;
|
||||||
|
this->valueHB = (value >> 8) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t OpenthermData::s16() {
|
||||||
|
int16_t const value = this->valueHB;
|
||||||
|
return (value << 8) | this->valueLB;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenthermData::s16(int16_t value) {
|
||||||
|
this->valueLB = value & 0xFF;
|
||||||
|
this->valueHB = (value >> 8) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
347
esphome/components/opentherm/opentherm.h
Normal file
347
esphome/components/opentherm/opentherm.h
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
/*
|
||||||
|
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
|
||||||
|
* heavily modified to comply with ESPHome coding standards and provide better logging.
|
||||||
|
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||||
|
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
#include "driver/timer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace opentherm {
|
||||||
|
|
||||||
|
// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR
|
||||||
|
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
||||||
|
|
||||||
|
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
||||||
|
|
||||||
|
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
||||||
|
|
||||||
|
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
||||||
|
return bit_value ? setBit(value, bit) : clearBit(value, bit);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OperationMode {
|
||||||
|
IDLE = 0, // no operation
|
||||||
|
|
||||||
|
LISTEN = 1, // waiting for transmission to start
|
||||||
|
READ = 2, // reading 32-bit data frame
|
||||||
|
RECEIVED = 3, // data frame received with valid start and stop bit
|
||||||
|
|
||||||
|
WRITE = 4, // writing data with timer_
|
||||||
|
SENT = 5, // all data written to output
|
||||||
|
|
||||||
|
ERROR_PROTOCOL = 8, // manchester protocol data transfer error
|
||||||
|
ERROR_TIMEOUT = 9 // read timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ProtocolErrorType {
|
||||||
|
NO_ERROR = 0, // No error
|
||||||
|
NO_TRANSITION = 1, // No transition in the middle of the bit
|
||||||
|
INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected
|
||||||
|
PARITY_ERROR = 3, // Parity check didn't pass
|
||||||
|
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
|
||||||
|
};
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
READ_DATA = 0,
|
||||||
|
READ_ACK = 4,
|
||||||
|
WRITE_DATA = 1,
|
||||||
|
WRITE_ACK = 5,
|
||||||
|
INVALID_DATA = 2,
|
||||||
|
DATA_INVALID = 6,
|
||||||
|
UNKNOWN_DATAID = 7
|
||||||
|
};
|
||||||
|
|
||||||
|
enum MessageId {
|
||||||
|
STATUS = 0,
|
||||||
|
CH_SETPOINT = 1,
|
||||||
|
CONTROLLER_CONFIG = 2,
|
||||||
|
DEVICE_CONFIG = 3,
|
||||||
|
COMMAND_CODE = 4,
|
||||||
|
FAULT_FLAGS = 5,
|
||||||
|
REMOTE = 6,
|
||||||
|
COOLING_CONTROL = 7,
|
||||||
|
CH2_SETPOINT = 8,
|
||||||
|
CH_SETPOINT_OVERRIDE = 9,
|
||||||
|
TSP_COUNT = 10,
|
||||||
|
TSP_COMMAND = 11,
|
||||||
|
FHB_SIZE = 12,
|
||||||
|
FHB_COMMAND = 13,
|
||||||
|
MAX_MODULATION_LEVEL = 14,
|
||||||
|
MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level
|
||||||
|
ROOM_SETPOINT = 16,
|
||||||
|
MODULATION_LEVEL = 17,
|
||||||
|
CH_WATER_PRESSURE = 18,
|
||||||
|
DHW_FLOW_RATE = 19,
|
||||||
|
DAY_TIME = 20,
|
||||||
|
DATE = 21,
|
||||||
|
YEAR = 22,
|
||||||
|
ROOM_SETPOINT_CH2 = 23,
|
||||||
|
ROOM_TEMP = 24,
|
||||||
|
FEED_TEMP = 25,
|
||||||
|
DHW_TEMP = 26,
|
||||||
|
OUTSIDE_TEMP = 27,
|
||||||
|
RETURN_WATER_TEMP = 28,
|
||||||
|
SOLAR_STORE_TEMP = 29,
|
||||||
|
SOLAR_COLLECT_TEMP = 30,
|
||||||
|
FEED_TEMP_CH2 = 31,
|
||||||
|
DHW2_TEMP = 32,
|
||||||
|
EXHAUST_TEMP = 33,
|
||||||
|
FAN_SPEED = 35,
|
||||||
|
FLAME_CURRENT = 36,
|
||||||
|
DHW_BOUNDS = 48,
|
||||||
|
CH_BOUNDS = 49,
|
||||||
|
OTC_CURVE_BOUNDS = 50,
|
||||||
|
DHW_SETPOINT = 56,
|
||||||
|
MAX_CH_SETPOINT = 57,
|
||||||
|
OTC_CURVE_RATIO = 58,
|
||||||
|
|
||||||
|
// HVAC Specific Message IDs
|
||||||
|
HVAC_STATUS = 70,
|
||||||
|
REL_VENT_SETPOINT = 71,
|
||||||
|
DEVICE_VENT = 74,
|
||||||
|
REL_VENTILATION = 77,
|
||||||
|
REL_HUMID_EXHAUST = 78,
|
||||||
|
SUPPLY_INLET_TEMP = 80,
|
||||||
|
SUPPLY_OUTLET_TEMP = 81,
|
||||||
|
EXHAUST_INLET_TEMP = 82,
|
||||||
|
EXHAUST_OUTLET_TEMP = 83,
|
||||||
|
NOM_REL_VENTILATION = 87,
|
||||||
|
|
||||||
|
OVERRIDE_FUNC = 100,
|
||||||
|
OEM_DIAGNOSTIC = 115,
|
||||||
|
BURNER_STARTS = 116,
|
||||||
|
CH_PUMP_STARTS = 117,
|
||||||
|
DHW_PUMP_STARTS = 118,
|
||||||
|
DHW_BURNER_STARTS = 119,
|
||||||
|
BURNER_HOURS = 120,
|
||||||
|
CH_PUMP_HOURS = 121,
|
||||||
|
DHW_PUMP_HOURS = 122,
|
||||||
|
DHW_BURNER_HOURS = 123,
|
||||||
|
OT_VERSION_CONTROLLER = 124,
|
||||||
|
OT_VERSION_DEVICE = 125,
|
||||||
|
VERSION_CONTROLLER = 126,
|
||||||
|
VERSION_DEVICE = 127
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BitPositions { STOP_BIT = 33 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure to hold Opentherm data packet content.
|
||||||
|
* Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message.
|
||||||
|
*/
|
||||||
|
struct OpenthermData {
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t id;
|
||||||
|
uint8_t valueHB;
|
||||||
|
uint8_t valueLB;
|
||||||
|
|
||||||
|
OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return float representation of data packet value
|
||||||
|
*/
|
||||||
|
float f88();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param float number to set as value of this data packet
|
||||||
|
*/
|
||||||
|
void f88(float value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return unsigned 16b integer representation of data packet value
|
||||||
|
*/
|
||||||
|
uint16_t u16();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param unsigned 16b integer number to set as value of this data packet
|
||||||
|
*/
|
||||||
|
void u16(uint16_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return signed 16b integer representation of data packet value
|
||||||
|
*/
|
||||||
|
int16_t s16();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param signed 16b integer number to set as value of this data packet
|
||||||
|
*/
|
||||||
|
void s16(int16_t value);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OpenThermError {
|
||||||
|
ProtocolErrorType error_type;
|
||||||
|
uint32_t capture;
|
||||||
|
uint8_t clock;
|
||||||
|
uint32_t data;
|
||||||
|
uint8_t bit_pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opentherm static class that supports either listening or sending Opentherm data packets in the same time
|
||||||
|
*/
|
||||||
|
class OpenTherm {
|
||||||
|
public:
|
||||||
|
OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup pins.
|
||||||
|
*/
|
||||||
|
bool initialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening for Opentherm data packet comming from line connected to given pin.
|
||||||
|
* If data packet is received then has_message() function returns true and data packet can be retrieved by calling
|
||||||
|
* get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and
|
||||||
|
* if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received
|
||||||
|
* or timeout is reached listening is stopped.
|
||||||
|
*/
|
||||||
|
void listen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to check whether listen() function already captured a valid data packet.
|
||||||
|
*
|
||||||
|
* @return true if data packet has been captured from line by listen() function.
|
||||||
|
*/
|
||||||
|
bool has_message() { return mode_ == OperationMode::RECEIVED; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function
|
||||||
|
* returns true. This function can be called multiple times until stop() is called.
|
||||||
|
*
|
||||||
|
* @param data reference to data structure to which fill the data packet data.
|
||||||
|
* @return true if packet was ready and was filled into data structure passed, false otherwise.
|
||||||
|
*/
|
||||||
|
bool get_message(OpenthermData &data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately send out Opentherm data packet to line connected on given pin.
|
||||||
|
* Completed data transfer is indicated by is_sent() function.
|
||||||
|
* Error state is indicated by is_error() function.
|
||||||
|
*
|
||||||
|
* @param data Opentherm data packet.
|
||||||
|
*/
|
||||||
|
void send(OpenthermData &data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops listening for data packet or sending out data packet and resets internal state of this class.
|
||||||
|
* Stops all timers and unattaches all interrupts.
|
||||||
|
*/
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get protocol error details in case a protocol error occured.
|
||||||
|
* @param error reference to data structure to which fill the error details
|
||||||
|
* @return true if protocol error occured during last conversation, false otherwise.
|
||||||
|
*/
|
||||||
|
bool get_protocol_error(OpenThermError &error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to check whether send() function already finished sending data packed to line.
|
||||||
|
*
|
||||||
|
* @return true if data packet has been sent, false otherwise.
|
||||||
|
*/
|
||||||
|
bool is_sent() { return mode_ == OperationMode::SENT; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether listinig or sending is not in progress.
|
||||||
|
* That also means that no timers are running and no interrupts are attached.
|
||||||
|
*
|
||||||
|
* @return true if listening nor sending is in progress.
|
||||||
|
*/
|
||||||
|
bool is_idle() { return mode_ == OperationMode::IDLE; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and
|
||||||
|
* protocol errors.
|
||||||
|
*
|
||||||
|
* @return true if last listen() or send() operation ends up with an error.
|
||||||
|
*/
|
||||||
|
bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether last listen() or send() operation ends up with a *timeout* error
|
||||||
|
* @return true if last listen() or send() operation ends up with a *timeout* error.
|
||||||
|
*/
|
||||||
|
bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether last listen() or send() operation ends up with a *protocol* error
|
||||||
|
* @return true if last listen() or send() operation ends up with a *protocol* error.
|
||||||
|
*/
|
||||||
|
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
|
||||||
|
|
||||||
|
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
|
||||||
|
|
||||||
|
OperationMode get_mode() { return mode_; }
|
||||||
|
|
||||||
|
std::string debug_data(OpenthermData &data);
|
||||||
|
std::string debug_error(OpenThermError &error);
|
||||||
|
|
||||||
|
const char *protocol_error_to_to_str(ProtocolErrorType error_type);
|
||||||
|
const char *message_type_to_str(MessageType message_type);
|
||||||
|
const char *operation_mode_to_str(OperationMode mode);
|
||||||
|
const char *message_id_to_str(MessageId id);
|
||||||
|
|
||||||
|
static bool timer_isr(OpenTherm *arg);
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
static void esp8266_timer_isr();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
InternalGPIOPin *in_pin_;
|
||||||
|
InternalGPIOPin *out_pin_;
|
||||||
|
ISRInternalGPIOPin isr_in_pin_;
|
||||||
|
ISRInternalGPIOPin isr_out_pin_;
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
timer_group_t timer_group_;
|
||||||
|
timer_idx_t timer_idx_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
OperationMode mode_;
|
||||||
|
ProtocolErrorType error_type_;
|
||||||
|
uint32_t capture_;
|
||||||
|
uint8_t clock_;
|
||||||
|
uint32_t data_;
|
||||||
|
uint8_t bit_pos_;
|
||||||
|
int32_t timeout_counter_; // <0 no timeout
|
||||||
|
|
||||||
|
int32_t device_timeout_;
|
||||||
|
|
||||||
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
||||||
|
bool init_esp32_timer_();
|
||||||
|
void start_esp32_timer_(uint64_t alarm_value);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void stop_timer_();
|
||||||
|
|
||||||
|
void read_(); // data detected start reading
|
||||||
|
void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz)
|
||||||
|
void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz)
|
||||||
|
bool check_parity_(uint32_t val);
|
||||||
|
|
||||||
|
void bit_read_(uint8_t value);
|
||||||
|
ProtocolErrorType verify_stop_bit_(uint8_t value);
|
||||||
|
void write_bit_(uint8_t high, uint8_t clock);
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
// ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
|
||||||
|
static OpenTherm *instance_;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace opentherm
|
||||||
|
} // namespace esphome
|
@ -8,8 +8,22 @@ namespace st7701s {
|
|||||||
void ST7701S::setup() {
|
void ST7701S::setup() {
|
||||||
esph_log_config(TAG, "Setting up ST7701S");
|
esph_log_config(TAG, "Setting up ST7701S");
|
||||||
this->spi_setup();
|
this->spi_setup();
|
||||||
|
this->write_init_sequence_();
|
||||||
|
}
|
||||||
|
|
||||||
|
// called after a delay after writing the init sequence
|
||||||
|
void ST7701S::complete_setup_() {
|
||||||
|
this->write_command_(SLEEP_OUT);
|
||||||
|
this->write_command_(DISPLAY_ON);
|
||||||
|
this->spi_teardown(); // SPI not needed after this
|
||||||
|
delay(10);
|
||||||
|
|
||||||
esp_lcd_rgb_panel_config_t config{};
|
esp_lcd_rgb_panel_config_t config{};
|
||||||
config.flags.fb_in_psram = 1;
|
config.flags.fb_in_psram = 1;
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
config.bounce_buffer_size_px = this->width_ * 10;
|
||||||
|
config.num_fbs = 1;
|
||||||
|
#endif // ESP_IDF_VERSION_MAJOR
|
||||||
config.timings.h_res = this->width_;
|
config.timings.h_res = this->width_;
|
||||||
config.timings.v_res = this->height_;
|
config.timings.v_res = this->height_;
|
||||||
config.timings.hsync_pulse_width = this->hsync_pulse_width_;
|
config.timings.hsync_pulse_width = this->hsync_pulse_width_;
|
||||||
@ -21,7 +35,6 @@ void ST7701S::setup() {
|
|||||||
config.timings.flags.pclk_active_neg = this->pclk_inverted_;
|
config.timings.flags.pclk_active_neg = this->pclk_inverted_;
|
||||||
config.timings.pclk_hz = this->pclk_frequency_;
|
config.timings.pclk_hz = this->pclk_frequency_;
|
||||||
config.clk_src = LCD_CLK_SRC_PLL160M;
|
config.clk_src = LCD_CLK_SRC_PLL160M;
|
||||||
config.sram_trans_align = 64;
|
|
||||||
config.psram_trans_align = 64;
|
config.psram_trans_align = 64;
|
||||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||||
for (size_t i = 0; i != data_pin_count; i++) {
|
for (size_t i = 0; i != data_pin_count; i++) {
|
||||||
@ -34,15 +47,21 @@ void ST7701S::setup() {
|
|||||||
config.de_gpio_num = this->de_pin_->get_pin();
|
config.de_gpio_num = this->de_pin_->get_pin();
|
||||||
config.pclk_gpio_num = this->pclk_pin_->get_pin();
|
config.pclk_gpio_num = this->pclk_pin_->get_pin();
|
||||||
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
|
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
|
||||||
|
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
|
||||||
|
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
|
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_));
|
|
||||||
ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_));
|
|
||||||
this->write_init_sequence_();
|
|
||||||
esph_log_config(TAG, "ST7701S setup complete");
|
esph_log_config(TAG, "ST7701S setup complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ST7701S::loop() {
|
||||||
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
|
if (this->handle_ != nullptr)
|
||||||
|
esp_lcd_rgb_panel_restart(this->handle_);
|
||||||
|
#endif // ESP_IDF_VERSION_MAJOR
|
||||||
|
}
|
||||||
|
|
||||||
void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||||
if (w <= 0 || h <= 0)
|
if (w <= 0 || h <= 0)
|
||||||
@ -160,10 +179,7 @@ void ST7701S::write_init_sequence_() {
|
|||||||
this->write_data_(val);
|
this->write_data_(val);
|
||||||
ESP_LOGD(TAG, "write MADCTL %X", val);
|
ESP_LOGD(TAG, "write MADCTL %X", val);
|
||||||
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
|
||||||
this->set_timeout(120, [this] {
|
this->set_timeout(120, [this] { this->complete_setup_(); });
|
||||||
this->write_command_(SLEEP_OUT);
|
|
||||||
this->write_command_(DISPLAY_ON);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ST7701S::dump_config() {
|
void ST7701S::dump_config() {
|
||||||
|
@ -33,6 +33,8 @@ class ST7701S : public display::Display,
|
|||||||
public:
|
public:
|
||||||
void update() override { this->do_update_(); }
|
void update() override { this->do_update_(); }
|
||||||
void setup() override;
|
void setup() override;
|
||||||
|
void complete_setup_();
|
||||||
|
void loop() override;
|
||||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import climate, sensor
|
from esphome.components import climate, sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AUTO_MODE,
|
CONF_AUTO_MODE,
|
||||||
CONF_AWAY_CONFIG,
|
CONF_AWAY_CONFIG,
|
||||||
@ -15,15 +15,15 @@ from esphome.const import (
|
|||||||
CONF_DRY_ACTION,
|
CONF_DRY_ACTION,
|
||||||
CONF_DRY_MODE,
|
CONF_DRY_MODE,
|
||||||
CONF_FAN_MODE,
|
CONF_FAN_MODE,
|
||||||
CONF_FAN_MODE_ON_ACTION,
|
|
||||||
CONF_FAN_MODE_OFF_ACTION,
|
|
||||||
CONF_FAN_MODE_AUTO_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_LOW_ACTION,
|
||||||
CONF_FAN_MODE_MEDIUM_ACTION,
|
CONF_FAN_MODE_MEDIUM_ACTION,
|
||||||
CONF_FAN_MODE_HIGH_ACTION,
|
|
||||||
CONF_FAN_MODE_MIDDLE_ACTION,
|
CONF_FAN_MODE_MIDDLE_ACTION,
|
||||||
CONF_FAN_MODE_FOCUS_ACTION,
|
CONF_FAN_MODE_OFF_ACTION,
|
||||||
CONF_FAN_MODE_DIFFUSE_ACTION,
|
CONF_FAN_MODE_ON_ACTION,
|
||||||
CONF_FAN_MODE_QUIET_ACTION,
|
CONF_FAN_MODE_QUIET_ACTION,
|
||||||
CONF_FAN_ONLY_ACTION,
|
CONF_FAN_ONLY_ACTION,
|
||||||
CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER,
|
CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER,
|
||||||
@ -50,8 +50,8 @@ from esphome.const import (
|
|||||||
CONF_MIN_HEATING_RUN_TIME,
|
CONF_MIN_HEATING_RUN_TIME,
|
||||||
CONF_MIN_IDLE_TIME,
|
CONF_MIN_IDLE_TIME,
|
||||||
CONF_MIN_TEMPERATURE,
|
CONF_MIN_TEMPERATURE,
|
||||||
CONF_NAME,
|
|
||||||
CONF_MODE,
|
CONF_MODE,
|
||||||
|
CONF_NAME,
|
||||||
CONF_OFF_MODE,
|
CONF_OFF_MODE,
|
||||||
CONF_PRESET,
|
CONF_PRESET,
|
||||||
CONF_SENSOR,
|
CONF_SENSOR,
|
||||||
@ -892,7 +892,7 @@ async def to_code(config):
|
|||||||
if name.upper() in climate.CLIMATE_PRESETS:
|
if name.upper() in climate.CLIMATE_PRESETS:
|
||||||
standard_preset = climate.CLIMATE_PRESETS[name.upper()]
|
standard_preset = climate.CLIMATE_PRESETS[name.upper()]
|
||||||
|
|
||||||
if two_points_available is True:
|
if two_points_available:
|
||||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
|
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
|
||||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
|
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
|
||||||
@ -905,6 +905,8 @@ async def to_code(config):
|
|||||||
preset_target_config = ThermostatClimateTargetTempConfig(
|
preset_target_config = ThermostatClimateTargetTempConfig(
|
||||||
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
|
preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
preset_target_config = None
|
||||||
|
|
||||||
preset_target_variable = cg.new_variable(
|
preset_target_variable = cg.new_variable(
|
||||||
preset_config[CONF_ID], preset_target_config
|
preset_config[CONF_ID], preset_target_config
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import binary_sensor
|
from esphome.components import binary_sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_KEY
|
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)
|
TM1638Key = tm1638_ns.class_("TM1638Key", binary_sensor.BinarySensor)
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import display
|
from esphome.components import display
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_CLK_PIN,
|
||||||
|
CONF_DIO_PIN,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_INTENSITY,
|
CONF_INTENSITY,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
CONF_CLK_PIN,
|
|
||||||
CONF_DIO_PIN,
|
|
||||||
CONF_STB_PIN,
|
CONF_STB_PIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,4 +51,4 @@ async def to_code(config):
|
|||||||
config[CONF_LAMBDA], [(TM1638ComponentRef, "it")], return_type=cg.void
|
config[CONF_LAMBDA], [(TM1638ComponentRef, "it")], return_type=cg.void
|
||||||
)
|
)
|
||||||
|
|
||||||
cg.add(var.set_writer(lambda_))
|
cg.add(var.set_writer(lambda_))
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import output
|
from esphome.components import output
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_LED
|
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)
|
TM1638OutputLed = tm1638_ns.class_("TM1638OutputLed", output.BinaryOutput, cg.Component)
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import switch
|
from esphome.components import switch
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_LED
|
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)
|
TM1638SwitchLed = tm1638_ns.class_("TM1638SwitchLed", switch.Switch, cg.Component)
|
||||||
|
|
||||||
|
@ -45,11 +45,8 @@ void UponorSmatrixComponent::loop() {
|
|||||||
|
|
||||||
// Read incoming data
|
// Read incoming data
|
||||||
while (this->available()) {
|
while (this->available()) {
|
||||||
// The controller polls devices every 10 seconds, with around 200 ms between devices.
|
// The controller polls devices every 10 seconds in some units or continuously in others with around 200 ms between
|
||||||
// Remember timestamps so we can send our own packets when the bus is expected to be silent.
|
// 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;
|
|
||||||
}
|
|
||||||
this->last_rx_ = now;
|
this->last_rx_ = now;
|
||||||
|
|
||||||
uint8_t byte;
|
uint8_t byte;
|
||||||
@ -60,7 +57,8 @@ void UponorSmatrixComponent::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send packets during bus silence
|
// 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
|
#ifdef USE_TIME
|
||||||
// Only build time packet when bus is silent and queue is empty to make sure we can send it right away
|
// 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_())
|
if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_())
|
||||||
|
@ -93,7 +93,6 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
|||||||
std::queue<std::vector<uint8_t>> tx_queue_;
|
std::queue<std::vector<uint8_t>> tx_queue_;
|
||||||
uint32_t last_rx_;
|
uint32_t last_rx_;
|
||||||
uint32_t last_tx_;
|
uint32_t last_tx_;
|
||||||
uint32_t last_poll_start_;
|
|
||||||
|
|
||||||
#ifdef USE_TIME
|
#ifdef USE_TIME
|
||||||
time::RealTimeClock *time_id_{nullptr};
|
time::RealTimeClock *time_id_{nullptr};
|
||||||
|
@ -755,7 +755,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
|||||||
message = std::move(arg.value);
|
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.
|
// Don't change state here since either the "tts-end" or "run-end" events will do it.
|
||||||
return;
|
return;
|
||||||
} else if (code == "wake-provider-missing" || code == "wake-engine-missing") {
|
} else if (code == "wake-provider-missing" || code == "wake-engine-missing") {
|
||||||
|
@ -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 {
|
class VoiceAssistant : public Component {
|
||||||
public:
|
public:
|
||||||
void setup() override;
|
void setup() override;
|
||||||
@ -133,6 +145,8 @@ class VoiceAssistant : public Component {
|
|||||||
void on_audio(const api::VoiceAssistantAudio &msg);
|
void on_audio(const api::VoiceAssistantAudio &msg);
|
||||||
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg);
|
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg);
|
||||||
void on_announce(const api::VoiceAssistantAnnounceRequest &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; }
|
bool is_running() const { return this->state_ != State::IDLE; }
|
||||||
void set_continuous(bool continuous) { this->continuous_ = continuous; }
|
void set_continuous(bool continuous) { this->continuous_ = continuous; }
|
||||||
@ -279,6 +293,8 @@ class VoiceAssistant : public Component {
|
|||||||
AudioMode audio_mode_{AUDIO_MODE_UDP};
|
AudioMode audio_mode_{AUDIO_MODE_UDP};
|
||||||
bool udp_socket_running_{false};
|
bool udp_socket_running_{false};
|
||||||
bool start_udp_socket_();
|
bool start_udp_socket_();
|
||||||
|
|
||||||
|
Configuration config_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<VoiceAssistant> {
|
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<VoiceAssistant> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Constants used by esphome."""
|
"""Constants used by esphome."""
|
||||||
|
|
||||||
__version__ = "2024.9.0-dev"
|
__version__ = "2024.10.0-dev"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
@ -852,6 +852,7 @@ CONF_TEMPERATURE_STEP = "temperature_step"
|
|||||||
CONF_TEXT = "text"
|
CONF_TEXT = "text"
|
||||||
CONF_TEXT_SENSORS = "text_sensors"
|
CONF_TEXT_SENSORS = "text_sensors"
|
||||||
CONF_THEN = "then"
|
CONF_THEN = "then"
|
||||||
|
CONF_THERMOCOUPLE_TYPE = "thermocouple_type"
|
||||||
CONF_THRESHOLD = "threshold"
|
CONF_THRESHOLD = "threshold"
|
||||||
CONF_THROTTLE = "throttle"
|
CONF_THROTTLE = "throttle"
|
||||||
CONF_TILT = "tilt"
|
CONF_TILT = "tilt"
|
||||||
|
@ -100,9 +100,6 @@ def valid_include(value):
|
|||||||
def valid_project_name(value: str):
|
def valid_project_name(value: str):
|
||||||
if value.count(".") != 1:
|
if value.count(".") != 1:
|
||||||
raise cv.Invalid("project name needs to have a namespace")
|
raise cv.Invalid("project name needs to have a namespace")
|
||||||
|
|
||||||
value = value.replace(" ", "_")
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
|||||||
extends = common:idf
|
extends = common:idf
|
||||||
platform = platformio/espressif32@5.4.0
|
platform = platformio/espressif32@5.4.0
|
||||||
platform_packages =
|
platform_packages =
|
||||||
platformio/framework-espidf@~3.40407.0
|
platformio/framework-espidf@~3.40408.0
|
||||||
|
|
||||||
framework = espidf
|
framework = espidf
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
pylint==3.1.0
|
pylint==3.2.7
|
||||||
flake8==7.0.0 # also change in .pre-commit-config.yaml when updating
|
flake8==7.0.0 # also change in .pre-commit-config.yaml when updating
|
||||||
black==24.4.2 # also change in .pre-commit-config.yaml when updating
|
black==24.4.2 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating
|
||||||
|
@ -10,3 +10,4 @@ sensor:
|
|||||||
cs_pin: 12
|
cs_pin: 12
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
mains_filter: 50Hz
|
mains_filter: 50Hz
|
||||||
|
thermocouple_type: N
|
||||||
|
@ -10,3 +10,5 @@ sensor:
|
|||||||
cs_pin: 8
|
cs_pin: 8
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
mains_filter: 50Hz
|
mains_filter: 50Hz
|
||||||
|
thermocouple_type: N
|
||||||
|
|
||||||
|
@ -10,3 +10,5 @@ sensor:
|
|||||||
cs_pin: 8
|
cs_pin: 8
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
mains_filter: 50Hz
|
mains_filter: 50Hz
|
||||||
|
thermocouple_type: N
|
||||||
|
|
||||||
|
@ -10,3 +10,4 @@ sensor:
|
|||||||
cs_pin: 12
|
cs_pin: 12
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
mains_filter: 50Hz
|
mains_filter: 50Hz
|
||||||
|
thermocouple_type: N
|
||||||
|
@ -10,3 +10,5 @@ sensor:
|
|||||||
cs_pin: 15
|
cs_pin: 15
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
mains_filter: 50Hz
|
mains_filter: 50Hz
|
||||||
|
thermocouple_type: N
|
||||||
|
|
||||||
|
@ -10,3 +10,5 @@ sensor:
|
|||||||
cs_pin: 6
|
cs_pin: 6
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
mains_filter: 50Hz
|
mains_filter: 50Hz
|
||||||
|
thermocouple_type: N
|
||||||
|
|
||||||
|
@ -29,3 +29,4 @@ modbus_controller:
|
|||||||
value_type: S_DWORD_R
|
value_type: S_DWORD_R
|
||||||
read_lambda: |-
|
read_lambda: |-
|
||||||
return 42.3;
|
return 42.3;
|
||||||
|
max_cmd_retries: 0
|
||||||
|
@ -13,3 +13,4 @@ modbus_controller:
|
|||||||
address: 0x2
|
address: 0x2
|
||||||
modbus_id: mod_bus1
|
modbus_id: mod_bus1
|
||||||
allow_duplicate_commands: true
|
allow_duplicate_commands: true
|
||||||
|
max_cmd_retries: 10
|
||||||
|
3
tests/components/opentherm/common.yaml
Normal file
3
tests/components/opentherm/common.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
opentherm:
|
||||||
|
in_pin: 1
|
||||||
|
out_pin: 2
|
1
tests/components/opentherm/test.esp32-ard.yaml
Normal file
1
tests/components/opentherm/test.esp32-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp32-c3-ard.yaml
Normal file
1
tests/components/opentherm/test.esp32-c3-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp32-c3-idf.yaml
Normal file
1
tests/components/opentherm/test.esp32-c3-idf.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp32-idf.yaml
Normal file
1
tests/components/opentherm/test.esp32-idf.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/opentherm/test.esp8266-ard.yaml
Normal file
1
tests/components/opentherm/test.esp8266-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
@ -21,4 +21,3 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 13
|
i2s_dout_pin: 13
|
||||||
mode: mono
|
|
||||||
|
@ -21,4 +21,3 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 3
|
i2s_dout_pin: 3
|
||||||
mode: mono
|
|
||||||
|
@ -21,4 +21,3 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 3
|
i2s_dout_pin: 3
|
||||||
mode: mono
|
|
||||||
|
@ -21,4 +21,3 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 13
|
i2s_dout_pin: 13
|
||||||
mode: mono
|
|
||||||
|
@ -28,7 +28,6 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 12
|
i2s_dout_pin: 12
|
||||||
mode: mono
|
|
||||||
|
|
||||||
voice_assistant:
|
voice_assistant:
|
||||||
microphone: mic_id_external
|
microphone: mic_id_external
|
||||||
|
@ -28,7 +28,6 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 2
|
i2s_dout_pin: 2
|
||||||
mode: mono
|
|
||||||
|
|
||||||
voice_assistant:
|
voice_assistant:
|
||||||
microphone: mic_id_external
|
microphone: mic_id_external
|
||||||
|
@ -28,7 +28,6 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 2
|
i2s_dout_pin: 2
|
||||||
mode: mono
|
|
||||||
|
|
||||||
voice_assistant:
|
voice_assistant:
|
||||||
microphone: mic_id_external
|
microphone: mic_id_external
|
||||||
|
@ -28,7 +28,6 @@ speaker:
|
|||||||
id: speaker_id
|
id: speaker_id
|
||||||
dac_type: external
|
dac_type: external
|
||||||
i2s_dout_pin: 12
|
i2s_dout_pin: 12
|
||||||
mode: mono
|
|
||||||
|
|
||||||
voice_assistant:
|
voice_assistant:
|
||||||
microphone: mic_id_external
|
microphone: mic_id_external
|
||||||
|
Loading…
x
Reference in New Issue
Block a user