Merge branch 'dev' into msa3xx

This commit is contained in:
Anton Viktorov 2024-09-30 13:27:42 +02:00 committed by GitHub
commit d2c79fbd3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
272 changed files with 7177 additions and 953 deletions

View file

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

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

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

View file

@ -65,7 +65,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.10.0
uses: pypa/gh-action-pypi-publish@v1.10.1
deploy-docker:
name: Build ESPHome ${{ matrix.platform }}

View file

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

View file

@ -61,7 +61,7 @@ esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas
esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme280_base/* @esphome/core
@ -70,6 +70,9 @@ esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bme68x_bsec2/* @kbx81 @neffs
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
esphome/components/bmi160/* @flaviut
esphome/components/bmp280_base/* @ademuri
esphome/components/bmp280_i2c/* @ademuri
esphome/components/bmp280_spi/* @ademuri
esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita
@ -83,7 +86,7 @@ esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @jesterret
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
@ -149,6 +152,7 @@ esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
@ -163,6 +167,7 @@ esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
@ -224,6 +229,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_8angle/* @rnauber
@ -286,6 +292,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931
@ -386,12 +393,14 @@ esphome/components/st7701s/* @clydebarrow
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/t6615/* @tylermenezes
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax

View file

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

View file

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

View file

@ -62,6 +62,8 @@ service APIConnection {
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {}
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
}
@ -1112,13 +1114,13 @@ enum MediaPlayerFormatPurpose {
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1;
}
message MediaPlayerSupportedFormat {
option (id) = 119;
option (ifdef) = "USE_MEDIA_PLAYER";
string format = 1;
uint32 sample_rate = 2;
uint32 num_channels = 3;
MediaPlayerFormatPurpose purpose = 4;
uint32 sample_bytes = 5;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
@ -1554,6 +1556,53 @@ message VoiceAssistantTimerEventResponse {
bool is_active = 6;
}
message VoiceAssistantAnnounceRequest {
option (id) = 119;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
string media_id = 1;
string text = 2;
}
message VoiceAssistantAnnounceFinished {
option (id) = 120;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VOICE_ASSISTANT";
bool success = 1;
}
message VoiceAssistantWakeWord {
string id = 1;
string wake_word = 2;
repeated string trained_languages = 3;
}
message VoiceAssistantConfigurationRequest {
option (id) = 121;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
}
message VoiceAssistantConfigurationResponse {
option (id) = 122;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated VoiceAssistantWakeWord available_wake_words = 1;
repeated string active_wake_words = 2;
uint32 max_active_wake_words = 3;
}
message VoiceAssistantSetConfiguration {
option (id) = 123;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated string active_wake_words = 1;
}
// ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0;

View file

@ -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.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes;
msg.supported_formats.push_back(media_format);
}
@ -1213,6 +1214,52 @@ void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistant
}
};
void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &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_announce(msg);
}
}
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));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
}
return resp;
}
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View file

@ -151,6 +151,10 @@ class APIConnection : public APIServerConnection {
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &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_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL

View file

@ -5149,6 +5149,10 @@ bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt va
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
return true;
}
case 5: {
this->sample_bytes = value.as_uint32();
return true;
}
default:
return false;
}
@ -5168,6 +5172,7 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->sample_rate);
buffer.encode_uint32(3, this->num_channels);
buffer.encode_enum<enums::MediaPlayerFormatPurpose>(4, this->purpose);
buffer.encode_uint32(5, this->sample_bytes);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
@ -5190,6 +5195,11 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
out.append(" purpose: ");
out.append(proto_enum_to_string<enums::MediaPlayerFormatPurpose>(this->purpose));
out.append("\n");
out.append(" sample_bytes: ");
sprintf(buffer, "%" PRIu32, this->sample_bytes);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@ -7061,6 +7071,193 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->media_id = value.as_string();
return true;
}
case 2: {
this->text = value.as_string();
return true;
}
default:
return false;
}
}
void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->media_id);
buffer.encode_string(2, this->text);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantAnnounceRequest {\n");
out.append(" media_id: ");
out.append("'").append(this->media_id).append("'");
out.append("\n");
out.append(" text: ");
out.append("'").append(this->text).append("'");
out.append("\n");
out.append("}");
}
#endif
bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->success = value.as_bool();
return true;
}
default:
return false;
}
}
void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); }
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantAnnounceFinished {\n");
out.append(" success: ");
out.append(YESNO(this->success));
out.append("\n");
out.append("}");
}
#endif
bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->id = value.as_string();
return true;
}
case 2: {
this->wake_word = value.as_string();
return true;
}
case 3: {
this->trained_languages.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->id);
buffer.encode_string(2, this->wake_word);
for (auto &it : this->trained_languages) {
buffer.encode_string(3, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantWakeWord::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantWakeWord {\n");
out.append(" id: ");
out.append("'").append(this->id).append("'");
out.append("\n");
out.append(" wake_word: ");
out.append("'").append(this->wake_word).append("'");
out.append("\n");
for (const auto &it : this->trained_languages) {
out.append(" trained_languages: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
#endif
void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
out.append("VoiceAssistantConfigurationRequest {}");
}
#endif
bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 3: {
this->max_active_wake_words = value.as_uint32();
return true;
}
default:
return false;
}
}
bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->available_wake_words.push_back(value.as_message<VoiceAssistantWakeWord>());
return true;
}
case 2: {
this->active_wake_words.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) {
buffer.encode_message<VoiceAssistantWakeWord>(1, it, true);
}
for (auto &it : this->active_wake_words) {
buffer.encode_string(2, it, true);
}
buffer.encode_uint32(3, this->max_active_wake_words);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantConfigurationResponse {\n");
for (const auto &it : this->available_wake_words) {
out.append(" available_wake_words: ");
it.dump_to(out);
out.append("\n");
}
for (const auto &it : this->active_wake_words) {
out.append(" active_wake_words: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append(" max_active_wake_words: ");
sprintf(buffer, "%" PRIu32, this->max_active_wake_words);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->active_wake_words.push_back(value.as_string());
return true;
}
default:
return false;
}
}
void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->active_wake_words) {
buffer.encode_string(1, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantSetConfiguration::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("VoiceAssistantSetConfiguration {\n");
for (const auto &it : this->active_wake_words) {
out.append(" active_wake_words: ");
out.append("'").append(it).append("'");
out.append("\n");
}
out.append("}");
}
#endif
bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {

View file

@ -1277,6 +1277,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1825,6 +1826,76 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantAnnounceRequest : public ProtoMessage {
public:
std::string media_id{};
std::string text{};
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 VoiceAssistantAnnounceFinished : public ProtoMessage {
public:
bool success{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantWakeWord : public ProtoMessage {
public:
std::string id{};
std::string wake_word{};
std::vector<std::string> trained_languages{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class VoiceAssistantConfigurationRequest : public ProtoMessage {
public:
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class VoiceAssistantConfigurationResponse : public ProtoMessage {
public:
std::vector<VoiceAssistantWakeWord> available_wake_words{};
std::vector<std::string> active_wake_words{};
uint32_t max_active_wake_words{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantSetConfiguration : public ProtoMessage {
public:
std::vector<std::string> active_wake_words{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
public:
std::string object_id{};

View file

@ -311,14 +311,6 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit
#ifdef USE_BUTTON
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_media_player_supported_format(const MediaPlayerSupportedFormat &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_media_player_supported_format: %s", msg.dump().c_str());
#endif
return this->send_message_<MediaPlayerSupportedFormat>(msg, 119);
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str());
@ -494,6 +486,29 @@ bool APIServerConnectionBase::send_voice_assistant_audio(const VoiceAssistantAud
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_VOICE_ASSISTANT
bool APIServerConnectionBase::send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_voice_assistant_announce_finished: %s", msg.dump().c_str());
#endif
return this->send_message_<VoiceAssistantAnnounceFinished>(msg, 120);
}
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_VOICE_ASSISTANT
bool APIServerConnectionBase::send_voice_assistant_configuration_response(
const VoiceAssistantConfigurationResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str());
#endif
return this->send_message_<VoiceAssistantConfigurationResponse>(msg, 122);
}
#endif
#ifdef USE_VOICE_ASSISTANT
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response(
const ListEntitiesAlarmControlPanelResponse &msg) {
@ -1147,13 +1162,35 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
case 119: {
#ifdef USE_MEDIA_PLAYER
MediaPlayerSupportedFormat msg;
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantAnnounceRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_media_player_supported_format: %s", msg.dump().c_str());
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
#endif
this->on_media_player_supported_format(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
break;
}
@ -1644,6 +1681,35 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
this->subscribe_voice_assistant(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
if (!this->send_voice_assistant_configuration_response(ret)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->voice_assistant_set_configuration(msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
if (!this->is_connection_setup()) {

View file

@ -145,10 +145,6 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_BUTTON
virtual void on_button_command_request(const ButtonCommandRequest &value){};
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_supported_format(const MediaPlayerSupportedFormat &msg);
virtual void on_media_player_supported_format(const MediaPlayerSupportedFormat &value){};
#endif
#ifdef USE_MEDIA_PLAYER
bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg);
#endif
@ -251,6 +247,21 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg);
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg);
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg);
#endif
@ -423,6 +434,13 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
#endif
@ -524,6 +542,12 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
#endif

View file

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

View file

@ -145,8 +145,9 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
),
)
async def reset_energy_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def to_code(config):

View file

@ -1 +1 @@
CODEOWNERS = ["@dbuezas"]
CODEOWNERS = ["@dbuezas", "@dwmw2"]

View file

@ -41,20 +41,33 @@ static const uint32_t BL0942_REG_MODE_DEFAULT =
static const uint32_t BL0942_REG_SOFT_RESET_MAGIC = 0x5a5a5a;
static const uint32_t BL0942_REG_USR_WRPROT_MAGIC = 0x55;
// 23-byte packet, 11 bits per byte, 2400 baud: about 105ms
static const uint32_t PKT_TIMEOUT_MS = 200;
void BL0942::loop() {
DataPacket buffer;
if (!this->available()) {
int avail = this->available();
if (!avail) {
return;
}
if (avail < sizeof(buffer)) {
if (!this->rx_start_) {
this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail);
this->read_array((uint8_t *) &buffer, avail);
this->rx_start_ = 0;
}
return;
}
if (this->read_array((uint8_t *) &buffer, sizeof(buffer))) {
if (this->validate_checksum_(&buffer)) {
this->received_package_(&buffer);
}
} else {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
while (read() >= 0)
;
}
this->rx_start_ = 0;
}
bool BL0942::validate_checksum_(DataPacket *data) {
@ -109,8 +122,23 @@ void BL0942::update() {
}
void BL0942::setup() {
// If either current or voltage references are set explicitly by the user,
// calculate the power reference from it unless that is also explicitly set.
if ((this->current_reference_set_ || this->voltage_reference_set_) && !this->power_reference_set_) {
this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0;
this->power_reference_set_ = true;
}
// Similarly for energy reference, if the power reference was set by the user
// either implicitly or explicitly.
if (this->power_reference_set_ && !this->energy_reference_set_) {
this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4;
this->energy_reference_set_ = true;
}
this->write_reg_(BL0942_REG_USR_WRPROT, BL0942_REG_USR_WRPROT_MAGIC);
this->write_reg_(BL0942_REG_SOFT_RESET, BL0942_REG_SOFT_RESET_MAGIC);
if (this->reset_)
this->write_reg_(BL0942_REG_SOFT_RESET, BL0942_REG_SOFT_RESET_MAGIC);
uint32_t mode = BL0942_REG_MODE_DEFAULT;
mode |= BL0942_REG_MODE_RMS_UPDATE_SEL; /* 800ms refresh time */
@ -133,10 +161,17 @@ void BL0942::received_package_(DataPacket *data) {
return;
}
// cf_cnt is only 24 bits, so track overflows
uint32_t cf_cnt = (uint24_t) data->cf_cnt;
cf_cnt |= this->prev_cf_cnt_ & 0xff000000;
if (cf_cnt < this->prev_cf_cnt_) {
cf_cnt += 0x1000000;
}
this->prev_cf_cnt_ = cf_cnt;
float v_rms = (uint24_t) data->v_rms / voltage_reference_;
float i_rms = (uint24_t) data->i_rms / current_reference_;
float watt = (int24_t) data->watt / power_reference_;
uint32_t cf_cnt = (uint24_t) data->cf_cnt;
float total_energy_consumption = cf_cnt / energy_reference_;
float frequency = 1000000.0f / data->frequency;
@ -162,13 +197,18 @@ void BL0942::received_package_(DataPacket *data) {
void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity)
ESP_LOGCONFIG(TAG, "BL0942:");
ESP_LOGCONFIG(TAG, " Reset: %s", TRUEFALSE(this->reset_));
ESP_LOGCONFIG(TAG, " Address: %d", this->address_);
ESP_LOGCONFIG(TAG, " Nominal line frequency: %d Hz", this->line_freq_);
ESP_LOGCONFIG(TAG, " Current reference: %f", this->current_reference_);
ESP_LOGCONFIG(TAG, " Energy reference: %f", this->energy_reference_);
ESP_LOGCONFIG(TAG, " Power reference: %f", this->power_reference_);
ESP_LOGCONFIG(TAG, " Voltage reference: %f", this->voltage_reference_);
LOG_SENSOR("", "Voltage", this->voltage_sensor_);
LOG_SENSOR("", "Current", this->current_sensor_);
LOG_SENSOR("", "Power", this->power_sensor_);
LOG_SENSOR("", "Energy", this->energy_sensor_);
LOG_SENSOR("", "frequency", this->frequency_sensor_);
LOG_SENSOR("", "Frequency", this->frequency_sensor_);
}
} // namespace bl0942

View file

@ -8,6 +8,57 @@
namespace esphome {
namespace bl0942 {
// The BL0942 IC is "calibration-free", which means that it doesn't care
// at all about calibration, and that's left to software. It measures a
// voltage differential on its IP/IN pins which linearly proportional to
// the current flow, and another on its VP pin which is proportional to
// the line voltage. It never knows the actual calibration; the values
// it reports are solely in terms of those inputs.
//
// The datasheet refers to the input voltages as I(A) and V(V), both
// in millivolts. It measures them against a reference voltage Vref,
// which is typically 1.218V (but that absolute value is meaningless
// without the actual calibration anyway).
//
// The reported I_RMS value is 305978 I(A)/Vref, and the reported V_RMS
// value is 73989 V(V)/Vref. So we can calibrate those by applying a
// simple meter with a resistive load.
//
// The chip also measures the phase difference between voltage and
// current, and uses it to calculate the power factor (cos φ). It
// reports the WATT value of 3537 * I_RMS * V_RMS * cos φ).
//
// It also integrates total energy based on the WATT value. The time for
// one CF_CNT pulse is 1638.4*256 / WATT.
//
// So... how do we calibrate that?
//
// Using a simple resistive load and an external meter, we can measure
// the true voltage and current for a given V_RMS and I_RMS reading,
// to calculate BL0942_UREF and BL0942_IREF. Those are in units of
// "305978 counts per amp" or "73989 counts per volt" respectively.
//
// We can derive BL0942_PREF from those. Let's eliminate the weird
// factors and express the calibration in plain counts per volt/amp:
// UREF1 = UREF/73989, IREF1 = IREF/305978.
//
// Next... the true power in Watts is V * I * cos φ, so that's equal
// to WATT/3537 * IREF1 * UREF1. Which means
// BL0942_PREF = BL0942_UREF * BL0942_IREF * 3537 / 305978 / 73989.
//
// Finally the accumulated energy. The period of a CF_CNT count is
// 1638.4*256 / WATT seconds, or 419230.4 / WATT seconds. Which means
// the energy represented by a CN_CNT pulse is 419230.4 WATT-seconds.
// Factoring in the calibration, that's 419230.4 / BL0942_PREF actual
// Watt-seconds (or Joules, as the physicists like to call them).
//
// But we're not being physicists today; we we're being engineers, so
// we want to convert to kWh instead. Which we do by dividing by 1000
// and then by 3600, so the energy in kWh is
// CF_CNT * 419230.4 / BL0942_PREF / 3600000
//
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
static const float BL0942_PREF = 596; // taken from tasmota
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
@ -42,6 +93,23 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
void set_address(uint8_t address) { this->address_ = address; }
void set_reset(bool reset) { this->reset_ = reset; }
void set_current_reference(float current_ref) {
this->current_reference_ = current_ref;
this->current_reference_set_ = true;
}
void set_energy_reference(float energy_ref) {
this->energy_reference_ = energy_ref;
this->energy_reference_set_ = true;
}
void set_power_reference(float power_ref) {
this->power_reference_ = power_ref;
this->power_reference_set_ = true;
}
void set_voltage_reference(float voltage_ref) {
this->voltage_reference_ = voltage_ref;
this->voltage_reference_set_ = true;
}
void loop() override;
void update() override;
@ -59,14 +127,21 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
// Divide by this to turn into Watt
float power_reference_ = BL0942_PREF;
bool power_reference_set_ = false;
// Divide by this to turn into Volt
float voltage_reference_ = BL0942_UREF;
bool voltage_reference_set_ = false;
// Divide by this to turn into Ampere
float current_reference_ = BL0942_IREF;
bool current_reference_set_ = false;
// Divide by this to turn into kWh
float energy_reference_ = BL0942_EREF;
bool energy_reference_set_ = false;
uint8_t address_ = 0;
bool reset_ = false;
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
uint32_t rx_start_ = 0;
uint32_t prev_cf_cnt_ = 0;
bool validate_checksum_(DataPacket *data);
int read_reg_(uint8_t reg);

View file

@ -24,6 +24,12 @@ from esphome.const import (
UNIT_WATT,
)
CONF_CURRENT_REFERENCE = "current_reference"
CONF_ENERGY_REFERENCE = "energy_reference"
CONF_POWER_REFERENCE = "power_reference"
CONF_RESET = "reset"
CONF_VOLTAGE_REFERENCE = "voltage_reference"
DEPENDENCIES = ["uart"]
bl0942_ns = cg.esphome_ns.namespace("bl0942")
@ -53,19 +59,19 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=0,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
accuracy_decimals=0,
accuracy_decimals=2,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
@ -77,6 +83,11 @@ CONFIG_SCHEMA = (
),
),
cv.Optional(CONF_ADDRESS, default=0): cv.int_range(min=0, max=3),
cv.Optional(CONF_RESET, default=True): cv.boolean,
cv.Optional(CONF_CURRENT_REFERENCE): cv.float_,
cv.Optional(CONF_ENERGY_REFERENCE): cv.float_,
cv.Optional(CONF_POWER_REFERENCE): cv.float_,
cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_,
}
)
.extend(cv.polling_component_schema("60s"))
@ -106,3 +117,12 @@ async def to_code(config):
cg.add(var.set_frequency_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_reset(config[CONF_RESET]))
if (current_reference := config.get(CONF_CURRENT_REFERENCE, None)) is not None:
cg.add(var.set_current_reference(current_reference))
if (voltage_reference := config.get(CONF_VOLTAGE_REFERENCE, None)) is not None:
cg.add(var.set_voltage_reference(voltage_reference))
if (power_reference := config.get(CONF_POWER_REFERENCE, None)) is not None:
cg.add(var.set_power_reference(power_reference))
if (energy_reference := config.get(CONF_ENERGY_REFERENCE, None)) is not None:
cg.add(var.set_energy_reference(energy_reference))

View file

@ -65,9 +65,7 @@ CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_AUTO_CONNECT = "auto_connect"
# Espressif platformio framework is built with MAX_BLE_CONN to 3, so
# enforce this in yaml checks.
MULTI_CONF = 3
MULTI_CONF = True
CONFIG_SCHEMA = (
cv.Schema(

View file

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

View file

@ -1,96 +1,5 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONFIG_SCHEMA = cv.invalid(
"The bmp280 sensor component has been renamed to bmp280_i2c."
)
DEPENDENCIES = ["i2c"]
bmp280_ns = cg.esphome_ns.namespace("bmp280")
BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE,
"1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X,
"2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X,
"4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X,
"8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X,
"16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X,
}
BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF,
"2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X,
"4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X,
"8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X,
"16X": BMP280IIRFilter.BMP280_IIR_FILTER_16X,
}
BMP280Component = bmp280_ns.class_(
"BMP280Component", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP280Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x77))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View file

@ -0,0 +1,88 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
)
CODEOWNERS = ["@ademuri"]
bmp280_ns = cg.esphome_ns.namespace("bmp280_base")
BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE,
"1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X,
"2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X,
"4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X,
"8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X,
"16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X,
}
BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF,
"2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X,
"4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X,
"8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X,
"16X": BMP280IIRFilter.BMP280_IIR_FILTER_16X,
}
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
return var

View file

@ -1,9 +1,9 @@
#include "bmp280.h"
#include "bmp280_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bmp280 {
namespace bmp280_base {
static const char *const TAG = "bmp280.sensor";
@ -59,6 +59,14 @@ static const char *iir_filter_to_str(BMP280IIRFilter filter) {
void BMP280Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up BMP280...");
uint8_t chip_id = 0;
// Read the chip id twice, to work around a bug where the first read is 0.
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (!this->read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
@ -122,7 +130,6 @@ void BMP280Component::setup() {
}
void BMP280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMP280:");
LOG_I2C_DEVICE(this);
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with BMP280 failed!");
@ -262,5 +269,5 @@ uint16_t BMP280Component::read_u16_le_(uint8_t a_register) {
}
int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
} // namespace bmp280
} // namespace bmp280_base
} // namespace esphome

View file

@ -2,10 +2,9 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bmp280 {
namespace bmp280_base {
/// Internal struct storing the calibration values of an BMP280.
struct BMP280CalibrationData {
@ -50,8 +49,8 @@ enum BMP280IIRFilter {
BMP280_IIR_FILTER_16X = 0b100,
};
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
class BMP280Component : public PollingComponent, public i2c::I2CDevice {
/// This class implements support for the BMP280 Temperature+Pressure sensor.
class BMP280Component : public PollingComponent {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
@ -68,6 +67,11 @@ class BMP280Component : public PollingComponent, public i2c::I2CDevice {
float get_setup_priority() const override;
void update() override;
virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
protected:
/// Read the temperature value and store the calculated ambient temperature in t_fine.
float read_temperature_(int32_t *t_fine);
@ -90,5 +94,5 @@ class BMP280Component : public PollingComponent, public i2c::I2CDevice {
} error_code_{NONE};
};
} // namespace bmp280
} // namespace bmp280_base
} // namespace esphome

View file

@ -0,0 +1,27 @@
#include "bmp280_i2c.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bmp280_i2c {
bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
return I2CDevice::read_byte(a_register, data);
};
bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
return I2CDevice::write_byte(a_register, data);
};
bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
return I2CDevice::read_bytes(a_register, data, len);
};
bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
return I2CDevice::read_byte_16(a_register, data);
};
void BMP280I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BMP280Component::dump_config();
}
} // namespace bmp280_i2c
} // namespace esphome

View file

@ -0,0 +1,22 @@
#pragma once
#include "esphome/components/bmp280_base/bmp280_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bmp280_i2c {
static const char *const TAG = "bmp280_i2c.sensor";
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
public:
bool read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
void dump_config() override;
};
} // namespace bmp280_i2c
} // namespace esphome

View file

@ -0,0 +1,22 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from ..bmp280_base import to_code_base, CONFIG_SCHEMA_BASE
AUTO_LOAD = ["bmp280_base"]
CODEOWNERS = ["@ademuri"]
DEPENDENCIES = ["i2c"]
bmp280_ns = cg.esphome_ns.namespace("bmp280_i2c")
BMP280I2CComponent = bmp280_ns.class_(
"BMP280I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x77)
).extend({cv.GenerateID(): cv.declare_id(BMP280I2CComponent)})
async def to_code(config):
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,65 @@
#include <cstdint>
#include <cstddef>
#include "bmp280_spi.h"
#include <esphome/components/bmp280_base/bmp280_base.h>
namespace esphome {
namespace bmp280_spi {
uint8_t set_bit(uint8_t num, uint8_t position) {
uint8_t mask = 1 << position;
return num | mask;
}
uint8_t clear_bit(uint8_t num, uint8_t position) {
uint8_t mask = 1 << position;
return num & ~mask;
}
void BMP280SPIComponent::setup() {
this->spi_setup();
BMP280Component::setup();
};
// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used
// and replaced by a read/write bit (RW = 0 for write and RW = 1 for read).
// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf
bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
this->enable();
this->transfer_byte(set_bit(a_register, 7));
*data = this->transfer_byte(0);
this->disable();
return true;
}
bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
this->enable();
this->transfer_byte(clear_bit(a_register, 7));
this->transfer_byte(data);
this->disable();
return true;
}
bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
this->enable();
this->transfer_byte(set_bit(a_register, 7));
this->read_array(data, len);
this->disable();
return true;
}
bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
this->enable();
this->transfer_byte(set_bit(a_register, 7));
((uint8_t *) data)[1] = this->transfer_byte(0);
((uint8_t *) data)[0] = this->transfer_byte(0);
this->disable();
return true;
}
} // namespace bmp280_spi
} // namespace esphome

View file

@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/bmp280_base/bmp280_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace bmp280_spi {
class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
void setup() override;
bool read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
};
} // namespace bmp280_spi
} // namespace esphome

View file

@ -0,0 +1,22 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi
from ..bmp280_base import to_code_base, CONFIG_SCHEMA_BASE
AUTO_LOAD = ["bmp280_base"]
CODEOWNERS = ["@ademuri"]
DEPENDENCIES = ["spi"]
bmp280_ns = cg.esphome_ns.namespace("bmp280_spi")
BMP280SPIComponent = bmp280_ns.class_(
"BMP280SPIComponent", cg.PollingComponent, spi.SPIDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
spi.spi_device_schema(default_mode="mode3")
).extend({cv.GenerateID(): cv.declare_id(BMP280SPIComponent)})
async def to_code(config):
var = await to_code_base(config)
await spi.register_spi_device(var, config)

View file

@ -1,18 +1,20 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.i2c import I2CBus
import esphome.config_validation as cv
from esphome.const import (
CONF_I2C_ID,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_RESTORE_VALUE,
)
CODEOWNERS = ["@jesterret"]
CODEOWNERS = ["@jesterret", "@clydebarrow"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
ch422g_ns = cg.esphome_ns.namespace("ch422g")
@ -23,29 +25,36 @@ CH422GGPIOPin = ch422g_ns.class_(
)
CONF_CH422G = "ch422g"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(CH422GComponent),
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x24))
)
# Note that no address is configurable - each register in the CH422G has a dedicated i2c address
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(CH422GComponent),
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
# Can't use register_i2c_device because there is no CONF_ADDRESS
parent = await cg.get_variable(config[CONF_I2C_ID])
cg.add(var.set_i2c_bus(parent))
# This is used as a final validation step so that modes have been fully transformed.
def pin_mode_check(pin_config, _):
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
raise cv.Invalid("CH422G only supports input on pins 0-7")
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
raise cv.Invalid("CH422G only supports open drain output on pins 8-11")
CH422G_PIN_SCHEMA = pins.gpio_base_schema(
CH422GGPIOPin,
cv.int_range(min=0, max=7),
modes=[CONF_INPUT, CONF_OUTPUT],
cv.int_range(min=0, max=11),
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
).extend(
{
cv.Required(CONF_CH422G): cv.use_id(CH422GComponent),
@ -53,7 +62,7 @@ CH422G_PIN_SCHEMA = pins.gpio_base_schema(
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA, pin_mode_check)
async def ch422g_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_CH422G])

View file

@ -4,33 +4,33 @@
namespace esphome {
namespace ch422g {
const uint8_t CH422G_REG_IN = 0x26;
const uint8_t CH422G_REG_OUT = 0x38;
const uint8_t OUT_REG_DEFAULT_VAL = 0xdf;
static const uint8_t CH422G_REG_MODE = 0x24;
static const uint8_t CH422G_MODE_OUTPUT = 0x01; // enables output mode on 0-7
static const uint8_t CH422G_MODE_OPEN_DRAIN = 0x04; // enables open drain mode on 8-11
static const uint8_t CH422G_REG_IN = 0x26; // read reg for input bits
static const uint8_t CH422G_REG_OUT = 0x38; // write reg for output bits 0-7
static const uint8_t CH422G_REG_OUT_UPPER = 0x23; // write reg for output bits 8-11
static const char *const TAG = "ch422g";
void CH422GComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up CH422G...");
// Test to see if device exists
if (!this->read_inputs_()) {
// set outputs before mode
this->write_outputs_();
// Set mode and check for errors
if (!this->set_mode_(this->mode_value_) || !this->read_inputs_()) {
ESP_LOGE(TAG, "CH422G not detected at 0x%02X", this->address_);
this->mark_failed();
return;
}
// restore defaults over whatever got saved on last boot
if (!this->restore_value_) {
this->write_output_(OUT_REG_DEFAULT_VAL);
}
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
}
void CH422GComponent::loop() {
// Clear all the previously read flags.
this->pin_read_cache_ = 0x00;
this->pin_read_flags_ = 0x00;
}
void CH422GComponent::dump_config() {
@ -41,82 +41,99 @@ void CH422GComponent::dump_config() {
}
}
// ch422g doesn't have any flag support (needs docs?)
void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {}
void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
if (pin < 8) {
if (flags & gpio::FLAG_OUTPUT) {
this->mode_value_ |= CH422G_MODE_OUTPUT;
}
} else {
if (flags & gpio::FLAG_OPEN_DRAIN) {
this->mode_value_ |= CH422G_MODE_OPEN_DRAIN;
}
}
}
bool CH422GComponent::digital_read(uint8_t pin) {
if (this->pin_read_cache_ == 0 || this->pin_read_cache_ & (1 << pin)) {
if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) {
// Read values on first access or in case it's being read again in the same loop
this->read_inputs_();
}
this->pin_read_cache_ |= (1 << pin);
return this->state_mask_ & (1 << pin);
this->pin_read_flags_ |= (1 << pin);
return (this->input_bits_ & (1 << pin)) != 0;
}
void CH422GComponent::digital_write(uint8_t pin, bool value) {
if (value) {
this->write_output_(this->state_mask_ | (1 << pin));
this->output_bits_ |= (1 << pin);
} else {
this->write_output_(this->state_mask_ & ~(1 << pin));
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
bool CH422GComponent::read_inputs_() {
if (this->is_failed()) {
return false;
}
uint8_t temp = 0;
if ((this->last_error_ = this->read(&temp, 1)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str());
return false;
uint8_t result;
// reading inputs requires the chip to be in input mode, possibly temporarily.
if (this->mode_value_ & CH422G_MODE_OUTPUT) {
this->set_mode_(this->mode_value_ & ~CH422G_MODE_OUTPUT);
result = this->read_reg_(CH422G_REG_IN);
this->set_mode_(this->mode_value_);
} else {
result = this->read_reg_(CH422G_REG_IN);
}
uint8_t output = 0;
if ((this->last_error_ = this->bus_->read(CH422G_REG_IN, &output, 1)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str());
return false;
}
this->state_mask_ = output;
this->input_bits_ = result;
this->status_clear_warning();
return true;
}
bool CH422GComponent::write_output_(uint8_t value) {
const uint8_t temp = 1;
if ((this->last_error_ = this->write(&temp, 1, false)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("write_output_(): I2C I/O error: %d", (int) this->last_error_).c_str());
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
auto err = this->bus_->write(reg, &value, 1);
if (err != i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
return false;
}
uint8_t write_mask = value;
if ((this->last_error_ = this->bus_->write(CH422G_REG_OUT, &write_mask, 1)) != esphome::i2c::ERROR_OK) {
this->status_set_warning(
str_sprintf("write_output_(): I2C I/O error: %d for write_mask: %d", (int) this->last_error_, (int) write_mask)
.c_str());
return false;
}
this->state_mask_ = value;
this->status_clear_warning();
return true;
}
uint8_t CH422GComponent::read_reg_(uint8_t reg) {
uint8_t value;
auto err = this->bus_->read(reg, &value, 1);
if (err != i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
return 0;
}
this->status_clear_warning();
return value;
}
bool CH422GComponent::set_mode_(uint8_t mode) { return this->write_reg_(CH422G_REG_MODE, mode); }
bool CH422GComponent::write_outputs_() {
return this->write_reg_(CH422G_REG_OUT, static_cast<uint8_t>(this->output_bits_)) &&
this->write_reg_(CH422G_REG_OUT_UPPER, static_cast<uint8_t>(this->output_bits_ >> 8));
}
float CH422GComponent::get_setup_priority() const { return setup_priority::IO; }
// Run our loop() method very early in the loop, so that we cache read values
// before other components call our digital_read() method.
float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI
void CH422GGPIOPin::setup() { pin_mode(flags_); }
void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); }
void CH422GGPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags;
this->parent_->pin_mode(this->pin_, flags);
}
} // namespace ch422g
} // namespace esphome

View file

@ -23,32 +23,30 @@ class CH422GComponent : public Component, public i2c::I2CDevice {
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
float get_loop_priority() const override;
void dump_config() override;
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
bool write_reg_(uint8_t reg, uint8_t value);
uint8_t read_reg_(uint8_t reg);
bool set_mode_(uint8_t mode);
bool read_inputs_();
bool write_output_(uint8_t value);
bool write_outputs_();
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t state_mask_{0x00};
uint16_t output_bits_{0x00};
/// Flags to check if read previously during this loop
uint8_t pin_read_cache_ = {0x00};
/// Storage for last I2C error seen
esphome::i2c::ErrorCode last_error_;
/// Whether we want to override stored values on expander
bool restore_value_{false};
uint8_t pin_read_flags_ = {0x00};
/// Copy of last read values
uint8_t input_bits_ = {0x00};
/// Copy of the mode value
uint8_t mode_value_{};
};
/// Helper class to expose a CH422G pin as an internal input GPIO pin.
/// Helper class to expose a CH422G pin as a GPIO pin.
class CH422GGPIOPin : public GPIOPin {
public:
void setup() override;
void setup() override{};
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
@ -57,13 +55,13 @@ class CH422GGPIOPin : public GPIOPin {
void set_parent(CH422GComponent *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
void set_flags(gpio::Flags flags);
protected:
CH422GComponent *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
CH422GComponent *parent_{};
uint8_t pin_{};
bool inverted_{};
gpio::Flags flags_{};
};
} // namespace ch422g

View file

@ -147,6 +147,7 @@ void CSE7766Component::parse_data_() {
float power = 0.0f;
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
have_power = true;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
@ -178,6 +179,15 @@ void CSE7766Component::parse_data_() {
if (this->apparent_power_sensor_ != nullptr) {
this->apparent_power_sensor_->publish_state(apparent_power);
}
if (have_power && this->reactive_power_sensor_ != nullptr) {
const float reactive_power = apparent_power - power;
if (reactive_power < 0.0f) {
ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power);
this->reactive_power_sensor_->publish_state(0.0f);
} else {
this->reactive_power_sensor_->publish_state(reactive_power);
}
}
if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) {
float pf = NAN;
if (apparent_power > 0) {
@ -232,6 +242,7 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Power", this->power_sensor_);
LOG_SENSOR(" ", "Energy", this->energy_sensor_);
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800);
}

View file

@ -16,6 +16,9 @@ class CSE7766Component : public Component, public uart::UARTDevice {
void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) {
apparent_power_sensor_ = apparent_power_sensor;
}
void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) {
reactive_power_sensor_ = reactive_power_sensor;
}
void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; }
void loop() override;
@ -35,6 +38,7 @@ class CSE7766Component : public Component, public uart::UARTDevice {
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *energy_sensor_{nullptr};
sensor::Sensor *apparent_power_sensor_{nullptr};
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
uint16_t cf_pulses_last_{0};

View file

@ -8,18 +8,21 @@ from esphome.const import (
CONF_ID,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
UNIT_WATT_HOURS,
)
@ -62,6 +65,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_REACTIVE_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
@ -94,6 +103,9 @@ async def to_code(config):
if apparent_power_config := config.get(CONF_APPARENT_POWER):
sens = await sensor.new_sensor(apparent_power_config)
cg.add(var.set_apparent_power_sensor(sens))
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))

View file

@ -26,7 +26,6 @@ from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter", "@jesserockz"]
DEPENDENCIES = ["time"]
IS_PLATFORM_COMPONENT = True
@ -62,20 +61,28 @@ DATETIME_MODES = [
]
_DATETIME_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
def _validate_time_present(config):
config = config.copy()
if CONF_ON_TIME in config and CONF_TIME_ID not in config:
time_id = cv.use_id(time.RealTimeClock)(None)
config[CONF_TIME_ID] = time_id
return config
_DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
web_server.WEBSERVER_SORTING_SCHEMA,
cv.MQTT_COMMAND_COMPONENT_SCHEMA,
cv.Schema(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
}
),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
)
),
).add_extra(_validate_time_present)
def date_schema(class_: MockObjClass) -> cv.Schema:
@ -138,8 +145,9 @@ async def setup_datetime_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)
rtc = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_rtc(rtc))
if CONF_TIME_ID in config:
rtc = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_rtc(rtc))
for conf in config.get(CONF_ON_TIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])

View file

@ -4,8 +4,9 @@
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/time.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome {
namespace datetime {
@ -19,23 +20,29 @@ class DateTimeBase : public EntityBase {
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
#ifdef USE_TIME
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
time::RealTimeClock *get_rtc() const { return this->rtc_; }
#endif
protected:
CallbackManager<void()> state_callback_;
#ifdef USE_TIME
time::RealTimeClock *rtc_;
#endif
bool has_state_{false};
};
#ifdef USE_TIME
class DateTimeStateTrigger : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) {
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
}
};
#endif
} // namespace datetime
} // namespace esphome

View file

@ -192,6 +192,7 @@ void DateTimeEntityRestoreState::apply(DateTimeEntity *time) {
time->publish_state();
}
#ifdef USE_TIME
static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider
// there has been a drastic time synchronization
@ -245,6 +246,7 @@ bool OnDateTimeTrigger::matches_(const ESPTime &time) const {
time.day_of_month == this->parent_->day && time.hour == this->parent_->hour &&
time.minute == this->parent_->minute && time.second == this->parent_->second;
}
#endif
} // namespace datetime
} // namespace esphome

View file

@ -134,6 +134,7 @@ template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public
}
};
#ifdef USE_TIME
class OnDateTimeTrigger : public Trigger<>, public Component, public Parented<DateTimeEntity> {
public:
void loop() override;
@ -143,6 +144,7 @@ class OnDateTimeTrigger : public Trigger<>, public Component, public Parented<Da
optional<ESPTime> last_check_;
};
#endif
} // namespace datetime
} // namespace esphome

View file

@ -94,6 +94,7 @@ void TimeEntityRestoreState::apply(TimeEntity *time) {
time->publish_state();
}
#ifdef USE_TIME
static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider
// there has been a drastic time synchronization
@ -145,6 +146,7 @@ bool OnTimeTrigger::matches_(const ESPTime &time) const {
return time.is_valid() && time.hour == this->parent_->hour && time.minute == this->parent_->minute &&
time.second == this->parent_->second;
}
#endif
} // namespace datetime
} // namespace esphome

View file

@ -113,6 +113,7 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
}
};
#ifdef USE_TIME
class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEntity> {
public:
void loop() override;
@ -122,6 +123,7 @@ class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEn
optional<ESPTime> last_check_;
};
#endif
} // namespace datetime
} // namespace esphome

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble_server, output
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
AUTO_LOAD = ["esp32_ble_server"]
CODEOWNERS = ["@jesserockz"]
@ -11,13 +12,36 @@ CONF_AUTHORIZED_DURATION = "authorized_duration"
CONF_AUTHORIZER = "authorizer"
CONF_BLE_SERVER_ID = "ble_server_id"
CONF_IDENTIFY_DURATION = "identify_duration"
CONF_ON_PROVISIONED = "on_provisioned"
CONF_ON_PROVISIONING = "on_provisioning"
CONF_ON_START = "on_start"
CONF_ON_STOP = "on_stop"
CONF_STATUS_INDICATOR = "status_indicator"
CONF_WIFI_TIMEOUT = "wifi_timeout"
improv_ns = cg.esphome_ns.namespace("improv")
Error = improv_ns.enum("Error")
State = improv_ns.enum("State")
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
ESP32ImprovComponent = esp32_improv_ns.class_(
"ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent
)
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisionedTrigger", automation.Trigger.template()
)
ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisioningTrigger", automation.Trigger.template()
)
ESP32ImprovStartTrigger = esp32_improv_ns.class_(
"ESP32ImprovStartTrigger", automation.Trigger.template()
)
ESP32ImprovStateTrigger = esp32_improv_ns.class_(
"ESP32ImprovStateTrigger", automation.Trigger.template()
)
ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
"ESP32ImprovStoppedTrigger", automation.Trigger.template()
)
CONFIG_SCHEMA = cv.Schema(
@ -37,6 +61,37 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(
CONF_WIFI_TIMEOUT, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisionedTrigger
),
}
),
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisioningTrigger
),
}
),
cv.Optional(CONF_ON_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStoppedTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -63,3 +118,29 @@ async def to_code(config):
if CONF_STATUS_INDICATOR in config:
status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR])
cg.add(var.set_status_indicator(status_indicator))
use_state_callback = False
for conf in config.get(CONF_ON_PROVISIONED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_PROVISIONING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(State, "state"), (Error, "error")], conf
)
use_state_callback = True
for conf in config.get(CONF_ON_STOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
use_state_callback = True
if use_state_callback:
cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK")

View file

@ -0,0 +1,72 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#include "esp32_improv_component.h"
#include "esphome/core/automation.h"
#include <improv.h>
namespace esphome {
namespace esp32_improv {
class ESP32ImprovProvisionedTrigger : public Trigger<> {
public:
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONED && !parent->is_failed()) {
trigger();
}
});
}
};
class ESP32ImprovProvisioningTrigger : public Trigger<> {
public:
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONING && !parent->is_failed()) {
trigger();
}
});
}
};
class ESP32ImprovStartTrigger : public Trigger<> {
public:
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
!parent->is_failed()) {
trigger();
}
});
}
};
class ESP32ImprovStateTrigger : public Trigger<improv::State, improv::Error> {
public:
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (!parent->is_failed()) {
trigger(state, error);
}
});
}
};
class ESP32ImprovStoppedTrigger : public Trigger<> {
public:
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_STOPPED && !parent->is_failed()) {
trigger();
}
});
}
};
} // namespace esp32_improv
} // namespace esphome
#endif
#endif

View file

@ -68,7 +68,12 @@ void ESP32ImprovComponent::setup_characteristics() {
void ESP32ImprovComponent::loop() {
if (!global_ble_server->is_running()) {
this->state_ = improv::STATE_STOPPED;
if (this->state_ != improv::STATE_STOPPED) {
this->state_ = improv::STATE_STOPPED;
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
this->incoming_data_.clear();
return;
}
@ -217,6 +222,9 @@ void ESP32ImprovComponent::set_state_(improv::State state) {
service_data[7] = 0x00; // Reserved
esp32_ble::global_ble->advertising_set_service_data(service_data);
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
void ESP32ImprovComponent::set_error_(improv::Error error) {
@ -270,7 +278,7 @@ void ESP32ImprovComponent::dump_config() {
void ESP32ImprovComponent::process_incoming_data_() {
uint8_t length = this->incoming_data_[1];
ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
if (this->incoming_data_.size() - 3 == length) {
this->set_error_(improv::ERROR_NONE);
improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
@ -295,7 +303,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
@ -313,7 +321,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
this->incoming_data_.clear();
}
} else if (this->incoming_data_.size() - 2 > length) {
ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer...");
ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer...");
this->incoming_data_.clear();
} else {
ESP_LOGV(TAG, "Waiting for split data packets...");
@ -327,7 +335,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
if (this->authorizer_ != nullptr)
this->authorized_start_ = millis();
#endif
ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
wifi::global_wifi_component->clear_sta();
}

View file

@ -9,6 +9,10 @@
#include "esphome/components/esp32_ble_server/ble_server.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#include "esphome/core/automation.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
@ -42,6 +46,11 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent {
void stop() override;
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
this->state_callback_.add(std::move(callback));
}
#endif
#ifdef USE_BINARY_SENSOR
void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; }
#endif
@ -54,6 +63,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent {
void set_wifi_timeout(uint32_t wifi_timeout) { this->wifi_timeout_ = wifi_timeout; }
uint32_t get_wifi_timeout() const { return this->wifi_timeout_; }
improv::State get_improv_state() const { return this->state_; }
improv::Error get_improv_error_state() const { return this->error_state_; }
protected:
bool should_start_{false};
bool setup_complete_{false};
@ -84,6 +96,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent {
improv::State state_{improv::STATE_STOPPED};
improv::Error error_state_{improv::ERROR_NONE};
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
CallbackManager<void(improv::State, improv::Error)> state_callback_{};
#endif
bool status_indicator_state_{false};
void set_status_indicator_state_(bool state);

View file

@ -59,6 +59,7 @@ ETHERNET_TYPES = {
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
"W5500": EthernetType.ETHERNET_TYPE_W5500,
"OPENETH": EthernetType.ETHERNET_TYPE_OPENETH,
}
SPI_ETHERNET_TYPES = ["W5500"]
@ -171,6 +172,7 @@ CONFIG_SCHEMA = cv.All(
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
"OPENETH": BASE_SCHEMA,
},
upper=True,
),
@ -240,6 +242,9 @@ async def to_code(config):
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True)
elif config[CONF_TYPE] == "OPENETH":
cg.add_define("USE_ETHERNET_OPENETH")
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
else:
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))

View file

@ -120,6 +120,8 @@ void EthernetComponent::setup() {
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
#elif defined(USE_ETHERNET_OPENETH)
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
@ -143,6 +145,13 @@ void EthernetComponent::setup() {
#endif
switch (this->type_) {
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH: {
phy_config.autonego_timeout_ms = 1000;
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
@ -302,6 +311,10 @@ void EthernetComponent::dump_config() {
eth_type = "W5500";
break;
case ETHERNET_TYPE_OPENETH:
eth_type = "OPENETH";
break;
default:
eth_type = "Unknown";
break;

View file

@ -25,6 +25,7 @@ enum EthernetType {
ETHERNET_TYPE_KSZ8081,
ETHERNET_TYPE_KSZ8081RNA,
ETHERNET_TYPE_W5500,
ETHERNET_TYPE_OPENETH,
};
struct ManualIP {

View file

@ -0,0 +1,67 @@
#include "gp2y1010au0f.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace gp2y1010au0f {
static const char *const TAG = "gp2y1010au0f";
static const float MIN_VOLTAGE = 0.0f;
static const float MAX_VOLTAGE = 4.0f;
void GP2Y1010AU0FSensor::dump_config() {
LOG_SENSOR("", "Sharp GP2Y1010AU0F PM2.5 Sensor", this);
ESP_LOGCONFIG(TAG, " Sampling duration: %" PRId32 " ms", this->sample_duration_);
ESP_LOGCONFIG(TAG, " ADC voltage multiplier: %.3f", this->voltage_multiplier_);
LOG_UPDATE_INTERVAL(this);
}
void GP2Y1010AU0FSensor::update() {
is_sampling_ = true;
this->set_timeout("read", this->sample_duration_, [this]() {
this->is_sampling_ = false;
if (this->num_samples_ == 0)
return;
float mean = this->sample_sum_ / float(this->num_samples_);
ESP_LOGD(TAG, "ADC read voltage: %.3f V (mean from %" PRId32 " samples)", mean, this->num_samples_);
// PM2.5 calculation
// ref: https://www.howmuchsnow.com/arduino/airquality/
int16_t pm_2_5_value = 170 * mean;
this->publish_state(pm_2_5_value);
});
// reset readings
this->num_samples_ = 0;
this->sample_sum_ = 0.0f;
}
void GP2Y1010AU0FSensor::loop() {
if (!this->is_sampling_)
return;
// enable the internal IR LED
this->led_output_->turn_on();
// wait for the sensor to stabilize
delayMicroseconds(this->sample_wait_before_);
// perform a single sample
float read_voltage = this->source_->sample();
// disable the internal IR LED
this->led_output_->turn_off();
if (std::isnan(read_voltage))
return;
read_voltage = read_voltage * this->voltage_multiplier_ - this->voltage_offset_;
if (read_voltage < MIN_VOLTAGE || read_voltage > MAX_VOLTAGE)
return;
this->num_samples_++;
this->sample_sum_ += read_voltage;
}
} // namespace gp2y1010au0f
} // namespace esphome

View file

@ -0,0 +1,52 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/components/output/binary_output.h"
namespace esphome {
namespace gp2y1010au0f {
class GP2Y1010AU0FSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override {
// after the base sensor has been initialized
return setup_priority::DATA - 1.0f;
}
void set_adc_source(voltage_sampler::VoltageSampler *source) { source_ = source; }
void set_voltage_refs(float offset, float multiplier) {
this->voltage_offset_ = offset;
this->voltage_multiplier_ = multiplier;
}
void set_led_output(output::BinaryOutput *output) { led_output_ = output; }
protected:
// duration in ms of the sampling phase
uint32_t sample_duration_ = 100;
// duration in us of the wait before sampling
// ref: https://global.sharp/products/device/lineup/data/pdf/datasheet/gp2y1010au_appl_e.pdf
uint32_t sample_wait_before_ = 280;
// duration in us of the wait after sampling
// it seems no need to delay on purpose since one ADC sampling takes longer than that (300-400 us on ESP8266)
// uint32_t sample_wait_after_ = 40;
// the sampling source to read voltage from
voltage_sampler::VoltageSampler *source_;
// ADC voltage reading offset
float voltage_offset_ = 0.0f;
// ADC voltage reading multiplier
float voltage_multiplier_ = 1.0f;
// the binary output to control the sampling LED
output::BinaryOutput *led_output_;
float sample_sum_ = 0.0f;
uint32_t num_samples_ = 0;
bool is_sampling_ = false;
};
} // namespace gp2y1010au0f
} // namespace esphome

View file

@ -0,0 +1,61 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler, output
from esphome.const import (
CONF_SENSOR,
CONF_OUTPUT,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
)
DEPENDENCIES = ["output"]
AUTO_LOAD = ["voltage_sampler"]
CODEOWNERS = ["@zry98"]
CONF_ADC_VOLTAGE_OFFSET = "adc_voltage_offset"
CONF_ADC_VOLTAGE_MULTIPLIER = "adc_voltage_multiplier"
gp2y1010au0f_ns = cg.esphome_ns.namespace("gp2y1010au0f")
GP2Y1010AU0FSensor = gp2y1010au0f_ns.class_(
"GP2Y1010AU0FSensor", sensor.Sensor, cg.PollingComponent
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
GP2Y1010AU0FSensor,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_CHEMICAL_WEAPON,
)
.extend(
{
cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler),
cv.Optional(CONF_ADC_VOLTAGE_OFFSET, default=0.0): cv.float_,
cv.Optional(CONF_ADC_VOLTAGE_MULTIPLIER, default=1.0): cv.float_,
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
# the ADC sensor to read voltage from
adc_sensor = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_adc_source(adc_sensor))
cg.add(
var.set_voltage_refs(
config[CONF_ADC_VOLTAGE_OFFSET], config[CONF_ADC_VOLTAGE_MULTIPLIER]
)
)
# the binary output to control the module's internal IR LED
led_output = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_led_output(led_output))

View file

@ -0,0 +1,38 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/hal.h"
namespace esphome {
namespace gpio_expander {
/// @brief A class to cache the read state of a GPIO expander.
template<typename T, T N> class CachedGpioExpander {
public:
bool digital_read(T pin) {
if (!this->read_cache_invalidated_[pin]) {
this->read_cache_invalidated_[pin] = true;
return this->digital_read_cache(pin);
}
return this->digital_read_hw(pin);
}
void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); }
protected:
virtual bool digital_read_hw(T pin) = 0;
virtual bool digital_read_cache(T pin) = 0;
virtual void digital_write_hw(T pin, bool value) = 0;
void reset_pin_cache_() {
for (T i = 0; i < N; i++) {
this->read_cache_invalidated_[i] = false;
}
}
std::array<bool, N> read_cache_invalidated_{};
};
} // namespace gpio_expander
} // namespace esphome

View file

@ -114,7 +114,6 @@ SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
@ -240,7 +239,9 @@ CONFIG_SCHEMA = cv.All(
): cv.ensure_list(
cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True)
),
cv.Optional(CONF_BEEPER, default=True): cv.boolean,
cv.Optional(CONF_BEEPER): cv.invalid(
f"The {CONF_BEEPER} option is deprecated, use beeper_on/beeper_off actions or beeper switch for a haier platform instead"
),
cv.Optional(
CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
@ -254,7 +255,7 @@ CONFIG_SCHEMA = cv.All(
): cv.int_range(min=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE),
cv.Optional(
CONF_SUPPORTED_PRESETS,
default=["BOOST", "ECO", "SLEEP"], # No AWAY by default
default=["BOOST", "SLEEP"], # No AWAY by default
): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
),

View file

@ -52,8 +52,6 @@ bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::stead
HaierClimateBase::HaierClimateBase()
: haier_protocol_(*this),
protocol_phase_(ProtocolPhases::SENDING_INIT_1),
display_status_(true),
health_mode_(false),
force_send_control_(false),
forced_request_status_(false),
reset_protocol_request_(false),
@ -127,21 +125,34 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() {
}
#endif
bool HaierClimateBase::get_display_state() const { return this->display_status_; }
void HaierClimateBase::set_display_state(bool state) {
if (this->display_status_ != state) {
this->display_status_ = state;
this->force_send_control_ = true;
void HaierClimateBase::save_settings() {
HaierBaseSettings settings{this->get_health_mode(), this->get_display_state()};
if (!this->base_rtc_.save(&settings)) {
ESP_LOGW(TAG, "Failed to save settings");
}
}
bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
bool HaierClimateBase::get_display_state() const {
return (this->display_status_ == SwitchState::ON) || (this->display_status_ == SwitchState::PENDING_ON);
}
void HaierClimateBase::set_display_state(bool state) {
if (state != this->get_display_state()) {
this->display_status_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->force_send_control_ = true;
this->save_settings();
}
}
bool HaierClimateBase::get_health_mode() const {
return (this->health_mode_ == SwitchState::ON) || (this->health_mode_ == SwitchState::PENDING_ON);
}
void HaierClimateBase::set_health_mode(bool state) {
if (this->health_mode_ != state) {
this->health_mode_ = state;
if (state != this->get_health_mode()) {
this->health_mode_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->force_send_control_ = true;
this->save_settings();
}
}
@ -287,6 +298,14 @@ void HaierClimateBase::loop() {
}
this->process_phase(now);
this->haier_protocol_.loop();
#ifdef USE_SWITCH
if ((this->display_switch_ != nullptr) && (this->display_switch_->state != this->get_display_state())) {
this->display_switch_->publish_state(this->get_display_state());
}
if ((this->health_mode_switch_ != nullptr) && (this->health_mode_switch_->state != this->get_health_mode())) {
this->health_mode_switch_->publish_state(this->get_health_mode());
}
#endif // USE_SWITCH
}
void HaierClimateBase::process_protocol_reset() {
@ -329,6 +348,26 @@ bool HaierClimateBase::prepare_pending_action() {
ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::initialization() {
constexpr uint32_t restore_settings_version = 0xA77D21EF;
this->base_rtc_ =
global_preferences->make_preference<HaierBaseSettings>(this->get_object_id_hash() ^ restore_settings_version);
HaierBaseSettings recovered;
if (!this->base_rtc_.load(&recovered)) {
recovered = {false, true};
}
this->display_status_ = recovered.display_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->health_mode_ = recovered.health_mode ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
#ifdef USE_SWITCH
if (this->display_switch_ != nullptr) {
this->display_switch_->publish_state(this->get_display_state());
}
if (this->health_mode_switch_ != nullptr) {
this->health_mode_switch_->publish_state(this->get_health_mode());
}
#endif
}
void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGD("Control", "Control call");
if (!this->valid_connection()) {
@ -353,6 +392,22 @@ void HaierClimateBase::control(const ClimateCall &call) {
}
}
#ifdef USE_SWITCH
void HaierClimateBase::set_display_switch(switch_::Switch *sw) {
this->display_switch_ = sw;
if ((this->display_switch_ != nullptr) && (this->valid_connection())) {
this->display_switch_->publish_state(this->get_display_state());
}
}
void HaierClimateBase::set_health_mode_switch(switch_::Switch *sw) {
this->health_mode_switch_ = sw;
if ((this->health_mode_switch_ != nullptr) && (this->valid_connection())) {
this->health_mode_switch_->publish_state(this->get_health_mode());
}
}
#endif
void HaierClimateBase::HvacSettings::reset() {
this->valid = false;
this->mode.reset();

View file

@ -8,6 +8,10 @@
// HaierProtocol
#include <protocol/haier_protocol.h>
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
namespace esphome {
namespace haier {
@ -20,10 +24,24 @@ enum class ActionRequest : uint8_t {
START_STERI_CLEAN = 5, // only hOn
};
struct HaierBaseSettings {
bool health_mode;
bool display_state;
};
class HaierClimateBase : public esphome::Component,
public esphome::climate::Climate,
public esphome::uart::UARTDevice,
public haier_protocol::ProtocolStream {
#ifdef USE_SWITCH
public:
void set_display_switch(switch_::Switch *sw);
void set_health_mode_switch(switch_::Switch *sw);
protected:
switch_::Switch *display_switch_{nullptr};
switch_::Switch *health_mode_switch_{nullptr};
#endif
public:
HaierClimateBase();
HaierClimateBase(const HaierClimateBase &) = delete;
@ -82,7 +100,8 @@ class HaierClimateBase : public esphome::Component,
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming)
virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming)
virtual void initialization(){};
virtual void save_settings();
virtual void initialization();
virtual bool prepare_pending_action();
virtual void process_protocol_reset();
esphome::climate::ClimateTraits traits() override;
@ -127,13 +146,19 @@ class HaierClimateBase : public esphome::Component,
ActionRequest action;
esphome::optional<haier_protocol::HaierMessage> message;
};
enum class SwitchState {
OFF = 0b00,
ON = 0b01,
PENDING_OFF = 0b10,
PENDING_ON = 0b11,
};
haier_protocol::ProtocolHandler haier_protocol_;
ProtocolPhases protocol_phase_;
esphome::optional<PendingAction> action_request_;
uint8_t fan_mode_speed_;
uint8_t other_modes_fan_speed_;
bool display_status_;
bool health_mode_;
SwitchState display_status_{SwitchState::ON};
SwitchState health_mode_{SwitchState::OFF};
bool force_send_control_;
bool forced_request_status_;
bool reset_protocol_request_;
@ -148,6 +173,7 @@ class HaierClimateBase : public esphome::Component,
std::chrono::steady_clock::time_point last_status_request_; // To request AC status
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
CallbackManager<void(const char *, size_t)> status_message_callback_{};
ESPPreferenceObject base_rtc_;
};
class StatusMessageTrigger : public Trigger<const char *, size_t> {

View file

@ -31,9 +31,32 @@ HonClimate::HonClimate()
HonClimate::~HonClimate() {}
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
void HonClimate::set_beeper_state(bool state) {
if (state != this->settings_.beeper_state) {
this->settings_.beeper_state = state;
#ifdef USE_SWITCH
this->beeper_switch_->publish_state(state);
#endif
this->hon_rtc_.save(&this->settings_);
}
}
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
bool HonClimate::get_beeper_state() const { return this->settings_.beeper_state; }
void HonClimate::set_quiet_mode_state(bool state) {
if (state != this->get_quiet_mode_state()) {
this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
this->settings_.quiet_mode_state = state;
#ifdef USE_SWITCH
this->quiet_mode_switch_->publish_state(state);
#endif
this->hon_rtc_.save(&this->settings_);
}
}
bool HonClimate::get_quiet_mode_state() const {
return (this->quiet_mode_state_ == SwitchState::ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON);
}
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
return this->current_vertical_swing_;
@ -474,16 +497,19 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) {
}
void HonClimate::initialization() {
constexpr uint32_t restore_settings_version = 0xE834D8DCUL;
this->rtc_ = global_preferences->make_preference<HonSettings>(this->get_object_id_hash() ^ restore_settings_version);
HaierClimateBase::initialization();
constexpr uint32_t restore_settings_version = 0x57EB59DDUL;
this->hon_rtc_ =
global_preferences->make_preference<HonSettings>(this->get_object_id_hash() ^ restore_settings_version);
HonSettings recovered;
if (this->rtc_.load(&recovered)) {
if (this->hon_rtc_.load(&recovered)) {
this->settings_ = recovered;
} else {
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER};
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
}
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
}
haier_protocol::HaierMessage HonClimate::get_control_message() {
@ -519,8 +545,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN;
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
// Disabling boost and eco mode for Fan only
out_data->quiet_mode = 0;
// Disabling boost for Fan only
out_data->fast_mode = 0;
break;
case CLIMATE_MODE_COOL:
@ -582,47 +607,34 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
}
if (out_data->ac_power == 0) {
// If AC is off - no presets allowed
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break;
case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
// 10 degrees allowed only in heat mode
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
break;
case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 1;
out_data->ten_degree = 0;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
@ -638,10 +650,23 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
this->pending_horizontal_direction_.reset();
}
out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
{
// Quiet mode
if ((out_data->ac_power == 0) || (out_data->ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN)) {
// If AC is off or in fan only mode - no quiet mode allowed
out_data->quiet_mode = 0;
} else {
out_data->quiet_mode = this->get_quiet_mode_state() ? 1 : 0;
}
// Clean quiet mode state pending flag
this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01);
}
out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
out_data->display_status = this->display_status_ ? 1 : 0;
out_data->health_mode = this->health_mode_ ? 1 : 0;
out_data->display_status = this->get_display_state() ? 1 : 0;
this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
out_data->health_mode = this->get_health_mode() ? 1 : 0;
this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, this->real_control_packet_size_);
@ -765,6 +790,22 @@ void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::stri
}
#endif // USE_TEXT_SENSOR
#ifdef USE_SWITCH
void HonClimate::set_beeper_switch(switch_::Switch *sw) {
this->beeper_switch_ = sw;
if (this->beeper_switch_ != nullptr) {
this->beeper_switch_->publish_state(this->get_beeper_state());
}
}
void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
this->quiet_mode_switch_ = sw;
if (this->quiet_mode_switch_ != nullptr) {
this->quiet_mode_switch_->publish_state(this->settings_.quiet_mode_state);
}
}
#endif // USE_SWITCH
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
@ -827,9 +868,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
{
// Extra modes/presets
optional<ClimatePreset> old_preset = this->preset;
if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_ECO;
} else if (packet.control.fast_mode != 0) {
if (packet.control.fast_mode != 0) {
this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP;
@ -883,28 +922,26 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status != 0;
if (disp_status != this->display_status_) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else {
this->display_status_ = disp_status;
}
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status != 0;
if (disp_status != this->get_display_state()) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
}
}
}
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
// Health mode
if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
bool old_health_mode = this->get_health_mode();
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
should_publish = should_publish || (old_health_mode != this->get_health_mode());
}
{
CleaningState new_cleaning;
@ -958,17 +995,36 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
}
should_publish = should_publish || (old_mode != this->mode);
}
{
// Quiet mode, should be after climate mode
if ((this->mode != CLIMATE_MODE_FAN_ONLY) && (this->mode != CLIMATE_MODE_OFF) &&
((((uint8_t) this->quiet_mode_state_) & 0b10) == 0)) {
// In proper mode and not in pending state
bool new_quiet_mode = packet.control.quiet_mode != 0;
if (new_quiet_mode != this->get_quiet_mode_state()) {
this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF;
this->settings_.quiet_mode_state = new_quiet_mode;
this->hon_rtc_.save(&this->settings_);
}
}
}
{
// Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode;
if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes();
bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end();
bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end();
if (horizontal_swing_supported &&
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
if (vertical_swing_supported &&
(packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) {
this->swing_mode = CLIMATE_SWING_BOTH;
} else {
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
}
} else {
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
if (vertical_swing_supported &&
(packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) {
this->swing_mode = CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = CLIMATE_SWING_OFF;
@ -985,7 +1041,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
if (save_settings) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
this->rtc_.save(&this->settings_);
this->hon_rtc_.save(&this->settings_);
}
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
@ -1017,7 +1073,7 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
this->beeper_status_ ? ZERO_BUF : ONE_BUF, 2));
this->get_beeper_state() ? ZERO_BUF : ONE_BUF, 2));
}
// Health mode
{
@ -1025,13 +1081,16 @@ void HonClimate::fill_control_messages_queue_() {
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
this->health_mode_ ? ONE_BUF : ZERO_BUF, 2));
this->get_health_mode() ? ONE_BUF : ZERO_BUF, 2));
this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
}
// Climate mode
ClimateMode climate_mode = this->mode;
bool new_power = this->mode != CLIMATE_MODE_OFF;
uint8_t fan_mode_buf[] = {0x00, 0xFF};
uint8_t quiet_mode_buf[] = {0x00, 0xFF};
if (climate_control.mode.has_value()) {
climate_mode = climate_control.mode.value();
uint8_t buffer[2] = {0x00, 0x00};
switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF:
@ -1076,8 +1135,6 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode
// Disabling eco mode for Fan only
quiet_mode_buf[1] = 0;
break;
case CLIMATE_MODE_COOL:
new_power = true;
@ -1108,30 +1165,20 @@ void HonClimate::fill_control_messages_queue_() {
uint8_t away_mode_buf[] = {0x00, 0xFF};
if (!new_power) {
// If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_AWAY:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00;
break;
@ -1140,8 +1187,18 @@ void HonClimate::fill_control_messages_queue_() {
break;
}
}
{
// Quiet mode
if (new_power && (climate_mode != CLIMATE_MODE_FAN_ONLY) && this->get_quiet_mode_state()) {
quiet_mode_buf[1] = 0x01;
} else {
quiet_mode_buf[1] = 0x00;
}
// Clean quiet mode state pending flag
this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01);
}
auto presets = this->traits_.get_supported_presets();
if ((quiet_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_ECO) != presets.end()))) {
if (quiet_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +

View file

@ -10,6 +10,9 @@
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include "esphome/core/automation.h"
#include "haier_base.h"
#include "hon_packet.h"
@ -28,6 +31,8 @@ enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE
struct HonSettings {
hon_protocol::VerticalSwingMode last_vertiacal_swing;
hon_protocol::HorizontalSwingMode last_horizontal_swing;
bool beeper_state;
bool quiet_mode_state;
};
class HonClimate : public HaierClimateBase {
@ -86,6 +91,15 @@ class HonClimate : public HaierClimateBase {
protected:
void update_sub_text_sensor_(SubTextSensorType type, const std::string &value);
text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr};
#endif
#ifdef USE_SWITCH
public:
void set_beeper_switch(switch_::Switch *sw);
void set_quiet_mode_switch(switch_::Switch *sw);
protected:
switch_::Switch *beeper_switch_{nullptr};
switch_::Switch *quiet_mode_switch_{nullptr};
#endif
public:
HonClimate();
@ -95,6 +109,8 @@ class HonClimate : public HaierClimateBase {
void dump_config() override;
void set_beeper_state(bool state);
bool get_beeper_state() const;
void set_quiet_mode_state(bool state);
bool get_quiet_mode_state() const;
esphome::optional<hon_protocol::VerticalSwingMode> get_vertical_airflow() const;
void set_vertical_airflow(hon_protocol::VerticalSwingMode direction);
esphome::optional<hon_protocol::HorizontalSwingMode> get_horizontal_airflow() const;
@ -153,7 +169,6 @@ class HonClimate : public HaierClimateBase {
bool functions_[5];
};
bool beeper_status_;
CleaningState cleaning_status_;
bool got_valid_outdoor_temp_;
esphome::optional<hon_protocol::VerticalSwingMode> pending_vertical_direction_{};
@ -175,7 +190,8 @@ class HonClimate : public HaierClimateBase {
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
HonSettings settings_;
ESPPreferenceObject rtc_;
ESPPreferenceObject hon_rtc_;
SwitchState quiet_mode_state_{SwitchState::OFF};
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {

View file

@ -376,8 +376,10 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
}
}
}
out_data->display_status = this->display_status_ ? 0 : 1;
out_data->health_mode = this->health_mode_ ? 1 : 0;
out_data->display_status = this->get_display_state() ? 0 : 1;
this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
out_data->health_mode = this->get_health_mode() ? 1 : 0;
this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
sizeof(smartair2_protocol::HaierPacketControl));
}
@ -446,28 +448,26 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
}
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
}
{
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status == 0;
if (disp_status != this->display_status_) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else {
this->display_status_ = disp_status;
}
// Display status
// should be before "Climate mode" because it is changing this->mode
if (packet.control.ac_power != 0) {
// if AC is off display status always ON so process it only when AC is on
bool disp_status = packet.control.display_status == 0;
if (disp_status != this->get_display_state()) {
// Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display
this->force_send_control_ = true;
} else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
}
}
}
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
// Health mode
if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
bool old_health_mode = this->get_health_mode();
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
should_publish = should_publish || (old_health_mode != this->get_health_mode());
}
{
// Climate mode

View file

@ -0,0 +1,91 @@
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components import switch
from esphome.const import (
CONF_BEEPER,
CONF_DISPLAY,
ENTITY_CATEGORY_CONFIG,
)
from ..climate import (
CONF_HAIER_ID,
CONF_PROTOCOL,
HaierClimateBase,
haier_ns,
PROTOCOL_HON,
)
CODEOWNERS = ["@paveldn"]
BeeperSwitch = haier_ns.class_("BeeperSwitch", switch.Switch)
HealthModeSwitch = haier_ns.class_("HealthModeSwitch", switch.Switch)
DisplaySwitch = haier_ns.class_("DisplaySwitch", switch.Switch)
QuietModeSwitch = haier_ns.class_("QuietModeSwitch", switch.Switch)
# Haier switches
CONF_HEALTH_MODE = "health_mode"
CONF_QUIET_MODE = "quiet_mode"
# Additional icons
ICON_LEAF = "mdi:leaf"
ICON_LED_ON = "mdi:led-on"
ICON_VOLUME_HIGH = "mdi:volume-high"
ICON_VOLUME_OFF = "mdi:volume-off"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HaierClimateBase),
cv.Optional(CONF_DISPLAY): switch.switch_schema(
DisplaySwitch,
icon=ICON_LED_ON,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
cv.Optional(CONF_HEALTH_MODE): switch.switch_schema(
HealthModeSwitch,
icon=ICON_LEAF,
default_restore_mode="DISABLED",
),
# Beeper switch is only supported for HonClimate
cv.Optional(CONF_BEEPER): switch.switch_schema(
BeeperSwitch,
icon=ICON_VOLUME_HIGH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
# Quiet mode is only supported for HonClimate
cv.Optional(CONF_QUIET_MODE): switch.switch_schema(
QuietModeSwitch,
icon=ICON_VOLUME_OFF,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="DISABLED",
),
}
)
def _final_validate(config):
full_config = fv.full_config.get()
for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]:
# Check switches that are only supported for HonClimate
if config.get(switch_type):
climate_path = full_config.get_path_for_id(config[CONF_HAIER_ID])[:-1]
climate_conf = full_config.get_config_for_path(climate_path)
protocol_type = climate_conf.get(CONF_PROTOCOL)
if protocol_type.casefold() != PROTOCOL_HON.casefold():
raise cv.Invalid(
f"{switch_type} switch is only supported for hon climate"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
parent = await cg.get_variable(config[CONF_HAIER_ID])
for switch_type in [CONF_DISPLAY, CONF_HEALTH_MODE, CONF_BEEPER, CONF_QUIET_MODE]:
if conf := config.get(switch_type):
sw_var = await switch.new_switch(conf)
await cg.register_parented(sw_var, parent)
cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var))

View file

@ -0,0 +1,14 @@
#include "beeper.h"
namespace esphome {
namespace haier {
void BeeperSwitch::write_state(bool state) {
if (this->parent_->get_beeper_state() != state) {
this->parent_->set_beeper_state(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../hon_climate.h"
namespace esphome {
namespace haier {
class BeeperSwitch : public switch_::Switch, public Parented<HonClimate> {
public:
BeeperSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#include "display.h"
namespace esphome {
namespace haier {
void DisplaySwitch::write_state(bool state) {
if (this->parent_->get_display_state() != state) {
this->parent_->set_display_state(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../haier_base.h"
namespace esphome {
namespace haier {
class DisplaySwitch : public switch_::Switch, public Parented<HaierClimateBase> {
public:
DisplaySwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#include "health_mode.h"
namespace esphome {
namespace haier {
void HealthModeSwitch::write_state(bool state) {
if (this->parent_->get_health_mode() != state) {
this->parent_->set_health_mode(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../haier_base.h"
namespace esphome {
namespace haier {
class HealthModeSwitch : public switch_::Switch, public Parented<HaierClimateBase> {
public:
HealthModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,14 @@
#include "quiet_mode.h"
namespace esphome {
namespace haier {
void QuietModeSwitch::write_state(bool state) {
if (this->parent_->get_quiet_mode_state() != state) {
this->parent_->set_quiet_mode_state(state);
}
this->publish_state(state);
}
} // namespace haier
} // namespace esphome

View file

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../hon_climate.h"
namespace esphome {
namespace haier {
class QuietModeSwitch : public switch_::Switch, public Parented<HonClimate> {
public:
QuietModeSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace haier
} // namespace esphome

View file

@ -5,6 +5,19 @@ from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
CODEOWNERS = ["@OttoWinter", "@esphome/core"]
homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
def validate_entity_domain(platform, supported_domains):
def validator(config):
domain = config[CONF_ENTITY_ID].split(".", 1)[0]
if domain not in supported_domains:
raise cv.Invalid(
f"Entity ID {config[CONF_ENTITY_ID]} is not supported by the {platform} platform."
)
return config
return validator
HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema(
{
cv.Required(CONF_ENTITY_ID): cv.entity_id,

View file

@ -7,19 +7,32 @@ from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
validate_entity_domain,
)
CODEOWNERS = ["@Links2004"]
DEPENDENCIES = ["api"]
SUPPORTED_DOMAINS = [
"automation",
"fan",
"humidifier",
"input_boolean",
"light",
"remote",
"siren",
"switch",
]
HomeassistantSwitch = homeassistant_ns.class_(
"HomeassistantSwitch", switch.Switch, cg.Component
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
switch.switch_schema(HomeassistantSwitch)
.extend(cv.COMPONENT_SCHEMA)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_entity_domain("switch", SUPPORTED_DOMAINS),
)

View file

@ -42,9 +42,9 @@ void HomeassistantSwitch::write_state(bool state) {
api::HomeassistantServiceResponse resp;
if (state) {
resp.service = "switch.turn_on";
resp.service = "homeassistant.turn_on";
} else {
resp.service = "switch.turn_off";
resp.service = "homeassistant.turn_off";
}
api::HomeassistantServiceMap entity_id_kv;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,19 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_MODE
import esphome.codegen as cg
from esphome.components import esp32, speaker
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT
from .. import (
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
I2SAudioComponent,
CONF_LEFT,
CONF_MONO,
CONF_RIGHT,
CONF_STEREO,
I2SAudioOut,
i2s_audio_component_schema,
i2s_audio_ns,
register_i2s_audio_component,
)
CODEOWNERS = ["@jesserockz"]
@ -19,18 +23,30 @@ I2SAudioSpeaker = i2s_audio_ns.class_(
"I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
CONF_MUTE_PIN = "mute_pin"
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
INTERNAL_DAC_OPTIONS = {
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
i2s_comm_format_t = cg.global_ns.enum("i2s_comm_format_t")
I2C_COMM_FMT_OPTIONS = {
"stand_i2s": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_I2S,
"stand_msb": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MSB,
"stand_pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_SHORT,
"stand_pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_LONG,
"stand_max": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MAX,
"i2s_msb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_MSB,
"i2s_lsb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_LSB,
"pcm": i2s_comm_format_t.I2S_COMM_FORMAT_PCM,
"pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_SHORT,
"pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG,
}
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
@ -44,28 +60,43 @@ def validate_esp32_variant(config):
return config
BASE_SCHEMA = (
speaker.SPEAKER_SCHEMA.extend(
i2s_audio_component_schema(
I2SAudioSpeaker,
default_sample_rate=16000,
default_channel=CONF_MONO,
default_bits_per_sample="16bit",
)
)
.extend(
{
cv.Optional(
CONF_TIMEOUT, default="100ms"
): cv.positive_time_period_milliseconds,
}
)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": speaker.SPEAKER_SCHEMA.extend(
"internal": BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioSpeaker),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
).extend(cv.COMPONENT_SCHEMA),
"external": speaker.SPEAKER_SCHEMA.extend(
),
"external": BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioSpeaker),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
*EXTERNAL_DAC_OPTIONS, lower=True
cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum(
I2C_COMM_FMT_OPTIONS, lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
),
},
key=CONF_DAC_TYPE,
),
@ -76,12 +107,12 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await register_i2s_audio_component(var, config)
await speaker.register_speaker(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT]))
cg.add(var.set_timeout(config[CONF_TIMEOUT]))

View file

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

View file

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

View file

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

View file

@ -53,6 +53,8 @@ MODELS = {
"inkplate_10": InkplateModel.INKPLATE_10,
"inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS,
"inkplate_6_v2": InkplateModel.INKPLATE_6_V2,
"inkplate_5": InkplateModel.INKPLATE_5,
"inkplate_5_v2": InkplateModel.INKPLATE_5_V2,
}
CONFIG_SCHEMA = cv.All(

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