Merge pull request #7466 from esphome/bump-2024.9.0
Some checks are pending
CI / Check flake8 (push) Blocked by required conditions
CI / Test split components (push) Blocked by required conditions
CI / Create common environment (push) Waiting to run
CI / Check black (push) Blocked by required conditions
CI / Check pylint (push) Blocked by required conditions
CI / Check pyupgrade (push) Blocked by required conditions
CI / Run script/ci-custom (push) Blocked by required conditions
CI / Run pytest (push) Blocked by required conditions
CI / Check clang-format (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 IDF (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP8266 (push) Blocked by required conditions
CI / list-components (push) Blocked by required conditions
CI / Component test (push) Blocked by required conditions
CI / Split components for testing into 20 groups maximum (push) Blocked by required conditions
CI / CI Status (push) Blocked by required conditions
CI for docker images / Build docker containers (push) Waiting to run
YAML lint / yamllint (push) Waiting to run

2024.9.0
This commit is contained in:
Jesse Hills 2024-09-19 07:56:46 +12:00 committed by GitHub
commit 6fde5e0fed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
340 changed files with 7409 additions and 1192 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

View file

@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.2.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment

View file

@ -23,7 +23,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.2.0
with:
python-version: "3.11"

View file

@ -42,7 +42,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.2.0
with:
python-version: "3.9"
- name: Set up Docker Buildx

View file

@ -41,7 +41,7 @@ jobs:
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment

View file

@ -53,7 +53,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.2.0
with:
python-version: "3.x"
- name: Set up python environment
@ -65,7 +65,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.9.0
uses: pypa/gh-action-pypi-publish@v1.10.1
deploy-docker:
name: Build ESPHome ${{ matrix.platform }}
@ -85,7 +85,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.2.0
with:
python-version: "3.9"
@ -141,7 +141,7 @@ jobs:
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.4.0
with:
name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests

View file

@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.2.0
with:
python-version: 3.12
@ -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.0
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View file

@ -58,9 +58,10 @@ esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1750/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core
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
@ -69,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
@ -82,6 +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/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
@ -169,6 +174,7 @@ esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @OttoWinter @esphome/core
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004
@ -221,6 +227,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
@ -382,6 +389,7 @@ 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
@ -421,6 +429,7 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter

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

@ -38,7 +38,7 @@ from esphome.const import (
SECRETS_FILES,
)
from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import indent, is_ip_address
from esphome.helpers import indent, is_ip_address, get_bool_env
from esphome.log import Fore, color, setup_log
from esphome.util import (
get_serial_ports,
@ -731,7 +731,11 @@ POST_CONFIG_ACTIONS = {
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument(
"-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true"
"-v",
"--verbose",
help="Enable verbose ESPHome logs.",
action="store_true",
default=get_bool_env("ESPHOME_VERBOSE"),
)
options_parser.add_argument(
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"

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) {}
}
@ -1107,6 +1109,19 @@ enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_MUTE = 3;
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
}
enum MediaPlayerFormatPurpose {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0;
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1;
}
message MediaPlayerSupportedFormat {
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;
option (source) = SOURCE_SERVER;
@ -1122,6 +1137,8 @@ message ListEntitiesMediaPlayerResponse {
EntityCategory entity_category = 7;
bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
}
message MediaPlayerStateResponse {
option (id) = 64;
@ -1539,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

@ -1026,6 +1026,16 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause();
for (auto &supported_format : traits.get_supported_formats()) {
MediaPlayerSupportedFormat media_format;
media_format.format = supported_format.format;
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);
}
return this->send_list_entities_media_player_response(msg);
}
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
@ -1204,6 +1214,49 @@ 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));
}
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

@ -387,6 +387,18 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::Me
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<> const char *proto_enum_to_string<enums::MediaPlayerFormatPurpose>(enums::MediaPlayerFormatPurpose value) {
switch (value) {
case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT:
return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT";
case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT:
return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT";
default:
return "UNKNOWN";
}
}
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
template<>
const char *proto_enum_to_string<enums::BluetoothDeviceRequestType>(enums::BluetoothDeviceRequestType value) {
switch (value) {
@ -5123,6 +5135,74 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->sample_rate = value.as_uint32();
return true;
}
case 3: {
this->num_channels = value.as_uint32();
return true;
}
case 4: {
this->purpose = value.as_enum<enums::MediaPlayerFormatPurpose>();
return true;
}
case 5: {
this->sample_bytes = value.as_uint32();
return true;
}
default:
return false;
}
}
bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->format = value.as_string();
return true;
}
default:
return false;
}
}
void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->format);
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 {
__attribute__((unused)) char buffer[64];
out.append("MediaPlayerSupportedFormat {\n");
out.append(" format: ");
out.append("'").append(this->format).append("'");
out.append("\n");
out.append(" sample_rate: ");
sprintf(buffer, "%" PRIu32, this->sample_rate);
out.append(buffer);
out.append("\n");
out.append(" num_channels: ");
sprintf(buffer, "%" PRIu32, this->num_channels);
out.append(buffer);
out.append("\n");
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
bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
@ -5159,6 +5239,10 @@ bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLeng
this->icon = value.as_string();
return true;
}
case 9: {
this->supported_formats.push_back(value.as_message<MediaPlayerSupportedFormat>());
return true;
}
default:
return false;
}
@ -5182,6 +5266,9 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_bool(8, this->supports_pause);
for (auto &it : this->supported_formats) {
buffer.encode_message<MediaPlayerSupportedFormat>(9, it, true);
}
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
@ -5219,6 +5306,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
out.append(" supports_pause: ");
out.append(YESNO(this->supports_pause));
out.append("\n");
for (const auto &it : this->supported_formats) {
out.append(" supported_formats: ");
it.dump_to(out);
out.append("\n");
}
out.append("}");
}
#endif
@ -6978,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

@ -156,6 +156,10 @@ enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
};
enum MediaPlayerFormatPurpose : uint32_t {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0,
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1,
};
enum BluetoothDeviceRequestType : uint32_t {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0,
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1,
@ -1267,6 +1271,22 @@ class ButtonCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class MediaPlayerSupportedFormat : public ProtoMessage {
public:
std::string format{};
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;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesMediaPlayerResponse : public ProtoMessage {
public:
std::string object_id{};
@ -1277,6 +1297,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage {
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1805,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

@ -486,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) {
@ -1135,6 +1158,39 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str());
#endif
this->on_update_command_request(msg);
#endif
break;
}
case 119: {
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantAnnounceRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_announce_request(msg);
#endif
break;
}
case 121: {
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantConfigurationRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_configuration_request(msg);
#endif
break;
}
case 123: {
#ifdef USE_VOICE_ASSISTANT
VoiceAssistantSetConfiguration msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_set_configuration(msg);
#endif
break;
}
@ -1625,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

@ -247,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
@ -419,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
@ -520,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

@ -1,34 +1,34 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, spi
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
CONF_CURRENT,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_LINE_FREQUENCY,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_FREQUENCY,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REACTIVE_POWER,
CONF_REVERSE_ACTIVE_ENERGY,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
ICON_LIGHTBULB,
ICON_CURRENT_AC,
ICON_LIGHTBULB,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
UNIT_WATT_HOURS,
)
CONF_LINE_FREQUENCY = "line_frequency"
CONF_METER_CONSTANT = "meter_constant"
CONF_PL_CONST = "pl_const"
CONF_GAIN_PGA = "gain_pga"

View file

@ -7,6 +7,7 @@ from esphome.const import (
CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_LINE_FREQUENCY,
CONF_PHASE_A,
CONF_PHASE_ANGLE,
CONF_PHASE_B,
@ -39,7 +40,6 @@ from esphome.const import (
from . import atm90e32_ns
CONF_LINE_FREQUENCY = "line_frequency"
CONF_CHIP_TEMPERATURE = "chip_temperature"
CONF_GAIN_PGA = "gain_pga"
CONF_CURRENT_PHASES = "current_phases"

View file

@ -0,0 +1 @@
CODEOWNERS = ["@athom-tech", "@tarontop", "@jesserockz"]

View file

@ -0,0 +1,238 @@
#include "bl0906.h"
#include "constants.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bl0906 {
static const char *const TAG = "bl0906";
constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; }
// The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated;
constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) {
return (address + data->l + data->m + data->h) ^ 0xFF;
}
void BL0906::loop() {
if (this->current_channel_ == UINT8_MAX) {
return;
}
while (this->available())
this->flush();
if (this->current_channel_ == 0) {
// Temperature
this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_);
} else if (this->current_channel_ == 1) {
this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_);
this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_);
this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_);
} else if (this->current_channel_ == 2) {
this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_);
this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_);
this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_);
} else if (this->current_channel_ == 3) {
this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_);
this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_);
this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_);
} else if (this->current_channel_ == 4) {
this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_);
this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_);
this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_);
} else if (this->current_channel_ == 5) {
this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_);
this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_);
this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_);
} else if (this->current_channel_ == 6) {
this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_);
this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_);
this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 2) {
// Frequency
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_);
// Voltage
this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 1) {
// Total power
this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_);
// Total Energy
this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_);
} else {
this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage
return;
}
this->current_channel_++;
this->handle_actions_();
}
void BL0906::setup() {
while (this->available())
this->flush();
this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE));
// Calibration (1: register address; 2: value before calibration; 3: value after calibration)
this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1
this->bias_correction_(BL0906_RMSOS_2, 0.01500, 0);
this->bias_correction_(BL0906_RMSOS_3, 0.01400, 0);
this->bias_correction_(BL0906_RMSOS_4, 0.01300, 0);
this->bias_correction_(BL0906_RMSOS_5, 0.01200, 0);
this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6
this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD));
}
void BL0906::update() { this->current_channel_ = 0; }
size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) {
this->action_queue_.push_back(function);
return this->action_queue_.size();
}
void BL0906::handle_actions_() {
if (this->action_queue_.empty()) {
return;
}
ActionCallbackFuncPtr ptr_func = nullptr;
for (int i = 0; i < this->action_queue_.size(); i++) {
ptr_func = this->action_queue_[i];
if (ptr_func) {
ESP_LOGI(TAG, "HandleActionCallback[%d]...", i);
(this->*ptr_func)();
}
}
while (this->available()) {
this->read();
}
this->action_queue_.clear();
}
// Reset energy
void BL0906::reset_energy_() {
this->write_array(BL0906_INIT[0], 6);
delay(1);
this->flush();
ESP_LOGW(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_INIT[0][0], BL0906_INIT[0][1], BL0906_INIT[0][2],
BL0906_INIT[0][3], BL0906_INIT[0][4], BL0906_INIT[0][5]);
}
// Read data
void BL0906::read_data_(const uint8_t address, const float reference, sensor::Sensor *sensor) {
if (sensor == nullptr) {
return;
}
DataPacket buffer;
ube24_t data_u24;
sbe24_t data_s24;
float value = 0;
bool signed_result = reference == BL0906_TREF || reference == BL0906_WATT || reference == BL0906_PREF;
this->write_byte(BL0906_READ_COMMAND);
this->write_byte(address);
if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) {
if (bl0906_checksum(address, &buffer) == buffer.checksum) {
if (signed_result) {
data_s24.l = buffer.l;
data_s24.m = buffer.m;
data_s24.h = buffer.h;
} else {
data_u24.l = buffer.l;
data_u24.m = buffer.m;
data_u24.h = buffer.h;
}
} else {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
while (read() >= 0)
;
return;
}
}
// Power
if (reference == BL0906_PREF) {
value = (float) to_int32_t(data_s24) * reference;
}
// Total power
if (reference == BL0906_WATT) {
value = (float) to_int32_t(data_s24) * reference;
}
// Voltage, current, power, total power
if (reference == BL0906_UREF || reference == BL0906_IREF || reference == BL0906_EREF || reference == BL0906_CF) {
value = (float) to_uint32_t(data_u24) * reference;
}
// Frequency
if (reference == BL0906_FREF) {
value = reference / (float) to_uint32_t(data_u24);
}
// Chip temperature
if (reference == BL0906_TREF) {
value = (float) to_int32_t(data_s24);
value = (value - 64) * 12.5 / 59 - 40;
}
sensor->publish_state(value);
}
// RMS offset correction
void BL0906::bias_correction_(uint8_t address, float measurements, float correction) {
DataPacket data;
float ki = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; // Current coefficient
float i_rms0 = measurements * ki;
float i_rms = correction * ki;
int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256;
data.l = value << 24 >> 24;
data.m = value << 16 >> 24;
if (value < 0) {
data.h = (value << 8 >> 24) | 0b10000000;
}
data.address = bl0906_checksum(address, &data);
ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address);
this->write_byte(BL0906_WRITE_COMMAND);
this->write_byte(address);
this->write_byte(data.l);
this->write_byte(data.m);
this->write_byte(data.h);
this->write_byte(data.address);
}
void BL0906::dump_config() {
ESP_LOGCONFIG(TAG, "BL0906:");
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Current1", this->current_1_sensor_);
LOG_SENSOR(" ", "Current2", this->current_2_sensor_);
LOG_SENSOR(" ", "Current3", this->current_3_sensor_);
LOG_SENSOR(" ", "Current4", this->current_4_sensor_);
LOG_SENSOR(" ", "Current5", this->current_5_sensor_);
LOG_SENSOR(" ", "Current6", this->current_6_sensor_);
LOG_SENSOR(" ", "Power1", this->power_1_sensor_);
LOG_SENSOR(" ", "Power2", this->power_2_sensor_);
LOG_SENSOR(" ", "Power3", this->power_3_sensor_);
LOG_SENSOR(" ", "Power4", this->power_4_sensor_);
LOG_SENSOR(" ", "Power5", this->power_5_sensor_);
LOG_SENSOR(" ", "Power6", this->power_6_sensor_);
LOG_SENSOR(" ", "Energy1", this->energy_1_sensor_);
LOG_SENSOR(" ", "Energy2", this->energy_2_sensor_);
LOG_SENSOR(" ", "Energy3", this->energy_3_sensor_);
LOG_SENSOR(" ", "Energy4", this->energy_4_sensor_);
LOG_SENSOR(" ", "Energy5", this->energy_5_sensor_);
LOG_SENSOR(" ", "Energy6", this->energy_6_sensor_);
LOG_SENSOR(" ", "Total Power", this->total_power_sensor_);
LOG_SENSOR(" ", "Total Energy", this->total_energy_sensor_);
LOG_SENSOR(" ", "Frequency", this->frequency_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
}
} // namespace bl0906
} // namespace esphome

View file

@ -0,0 +1,96 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/datatypes.h"
// https://www.belling.com.cn/media/file_object/bel_product/BL0906/datasheet/BL0906_V1.02_cn.pdf
// https://www.belling.com.cn/media/file_object/bel_product/BL0906/guide/BL0906%20APP%20Note_V1.02.pdf
namespace esphome {
namespace bl0906 {
struct DataPacket { // NOLINT(altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
uint8_t h{0};
uint8_t checksum; // checksum
uint8_t address;
} __attribute__((packed));
struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
uint8_t h{0};
} __attribute__((packed));
struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
int8_t h{0};
} __attribute__((packed));
template<typename... Ts> class ResetEnergyAction;
class BL0906;
using ActionCallbackFuncPtr = void (BL0906::*)();
class BL0906 : public PollingComponent, public uart::UARTDevice {
SUB_SENSOR(voltage)
SUB_SENSOR(current_1)
SUB_SENSOR(current_2)
SUB_SENSOR(current_3)
SUB_SENSOR(current_4)
SUB_SENSOR(current_5)
SUB_SENSOR(current_6)
SUB_SENSOR(power_1)
SUB_SENSOR(power_2)
SUB_SENSOR(power_3)
SUB_SENSOR(power_4)
SUB_SENSOR(power_5)
SUB_SENSOR(power_6)
SUB_SENSOR(total_power)
SUB_SENSOR(energy_1)
SUB_SENSOR(energy_2)
SUB_SENSOR(energy_3)
SUB_SENSOR(energy_4)
SUB_SENSOR(energy_5)
SUB_SENSOR(energy_6)
SUB_SENSOR(total_energy)
SUB_SENSOR(frequency)
SUB_SENSOR(temperature)
public:
void loop() override;
void update() override;
void setup() override;
void dump_config() override;
protected:
template<typename... Ts> friend class ResetEnergyAction;
void reset_energy_();
void read_data_(uint8_t address, float reference, sensor::Sensor *sensor);
void bias_correction_(uint8_t address, float measurements, float correction);
uint8_t current_channel_{0};
size_t enqueue_action_(ActionCallbackFuncPtr function);
void handle_actions_();
private:
std::vector<ActionCallbackFuncPtr> action_queue_{};
};
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
public:
void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
};
} // namespace bl0906
} // namespace esphome

View file

@ -0,0 +1,4 @@
# const.py
ICON_ENERGY = "mdi:lightning-bolt"
ICON_FREQUENCY = "mdi:cosine-wave"
ICON_VOLTAGE = "mdi:sine-wave"

View file

@ -0,0 +1,122 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace bl0906 {
// Total power conversion
static const float BL0906_WATT = 16 * 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) /
(40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000);
// Total Energy conversion
static const float BL0906_CF = 16 * 4194304 * 0.032768 * 16 /
(3600000 * 16 *
(40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 /
(1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000))));
// Frequency conversion
static const float BL0906_FREF = 10000000;
// Temperature conversion
static const float BL0906_TREF = 12.5 / 59 - 40;
// Current conversion
static const float BL0906_IREF = 1.097 / (12875 * 1 * (5.1 + 5.1) * 1000 / 2000);
// Voltage conversion
static const float BL0906_UREF = 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (13162 * 1 * 100 * 1000);
// Power conversion
static const float BL0906_PREF = 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) /
(40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000);
// Energy conversion
static const float BL0906_EREF = 4194304 * 0.032768 * 16 /
(3600000 * 16 *
(40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 /
(1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000))));
// Current coefficient
static const float BL0906_KI = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097;
// Power coefficient
static const float BL0906_KP = 40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / 1.097 / 1.097 /
(20000 + 20000 + 20000 + 20000 + 20000);
static const uint8_t USR_WRPROT_WITABLE[6] = {0xCA, 0x9E, 0x55, 0x55, 0x00, 0xB7};
static const uint8_t USR_WRPROT_ONLYREAD[6] = {0xCA, 0x9E, 0x00, 0x00, 0x00, 0x61};
static const uint8_t BL0906_READ_COMMAND = 0x35;
static const uint8_t BL0906_WRITE_COMMAND = 0xCA;
// Register address
// Voltage
static const uint8_t BL0906_V_RMS = 0x16;
// Total power
static const uint8_t BL0906_WATT_SUM = 0X2C;
// Current1~6
static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1
static const uint8_t BL0906_I_2_RMS = 0x0E;
static const uint8_t BL0906_I_3_RMS = 0x0F;
static const uint8_t BL0906_I_4_RMS = 0x10;
static const uint8_t BL0906_I_5_RMS = 0x13;
static const uint8_t BL0906_I_6_RMS = 0x14; // current_6
// Power1~6
static const uint8_t BL0906_WATT_1 = 0X23; // power_1
static const uint8_t BL0906_WATT_2 = 0X24;
static const uint8_t BL0906_WATT_3 = 0X25;
static const uint8_t BL0906_WATT_4 = 0X26;
static const uint8_t BL0906_WATT_5 = 0X29;
static const uint8_t BL0906_WATT_6 = 0X2A; // power_6
// Active pulse count, unsigned
static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1
static const uint8_t BL0906_CF_2_CNT = 0X31;
static const uint8_t BL0906_CF_3_CNT = 0X32;
static const uint8_t BL0906_CF_4_CNT = 0X33;
static const uint8_t BL0906_CF_5_CNT = 0X36;
static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6
// Total active pulse count, unsigned
static const uint8_t BL0906_CF_SUM_CNT = 0X39;
// Voltage frequency cycle
static const uint8_t BL0906_FREQUENCY = 0X4E;
// Internal temperature
static const uint8_t BL0906_TEMPERATURE = 0X5E;
// Calibration register
// RMS gain adjustment register
static const uint8_t BL0906_RMSGN_1 = 0x6D; // Channel_1
static const uint8_t BL0906_RMSGN_2 = 0x6E;
static const uint8_t BL0906_RMSGN_3 = 0x6F;
static const uint8_t BL0906_RMSGN_4 = 0x70;
static const uint8_t BL0906_RMSGN_5 = 0x73;
static const uint8_t BL0906_RMSGN_6 = 0x74; // Channel_6
// RMS offset correction register
static const uint8_t BL0906_RMSOS_1 = 0x78; // Channel_1
static const uint8_t BL0906_RMSOS_2 = 0x79;
static const uint8_t BL0906_RMSOS_3 = 0x7A;
static const uint8_t BL0906_RMSOS_4 = 0x7B;
static const uint8_t BL0906_RMSOS_5 = 0x7E;
static const uint8_t BL0906_RMSOS_6 = 0x7F; // Channel_6
// Active power gain adjustment register
static const uint8_t BL0906_WATTGN_1 = 0xB7; // Channel_1
static const uint8_t BL0906_WATTGN_2 = 0xB8;
static const uint8_t BL0906_WATTGN_3 = 0xB9;
static const uint8_t BL0906_WATTGN_4 = 0xBA;
static const uint8_t BL0906_WATTGN_5 = 0xBD;
static const uint8_t BL0906_WATTGN_6 = 0xBE; // Channel_6
// User write protection setting register,
// You must first write 0x5555 to the write protection setting register before writing to other registers.
static const uint8_t BL0906_USR_WRPROT = 0x9E;
// Reset Register
static const uint8_t BL0906_SOFT_RESET = 0x9F;
const uint8_t BL0906_INIT[2][6] = {
// Reset to default
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
// Enable User Operation Write
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
} // namespace bl0906
} // namespace esphome

View file

@ -0,0 +1,184 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_CHANNEL,
CONF_CURRENT,
CONF_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_NAME,
CONF_POWER,
CONF_TEMPERATURE,
CONF_TOTAL_POWER,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
ICON_CURRENT_AC,
ICON_POWER,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_HERTZ,
UNIT_KILOWATT_HOURS,
UNIT_VOLT,
UNIT_WATT,
)
# Import ICONS not included in esphome's const.py, from the local components const.py
from .const import ICON_ENERGY, ICON_FREQUENCY, ICON_VOLTAGE
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["bl0906"]
CONF_TOTAL_ENERGY = "total_energy"
bl0906_ns = cg.esphome_ns.namespace("bl0906")
BL0906 = bl0906_ns.class_("BL0906", cg.PollingComponent, uart.UARTDevice)
ResetEnergyAction = bl0906_ns.class_("ResetEnergyAction", automation.Action)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BL0906),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
icon=ICON_FREQUENCY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_FREQUENCY,
unit_of_measurement=UNIT_HERTZ,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=UNIT_CELSIUS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
icon=ICON_VOLTAGE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
unit_of_measurement=UNIT_VOLT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema(
icon=ICON_POWER,
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER,
unit_of_measurement=UNIT_WATT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TOTAL_ENERGY): sensor.sensor_schema(
icon=ICON_ENERGY,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
unit_of_measurement=UNIT_KILOWATT_HOURS,
),
}
)
.extend(
cv.Schema(
{
cv.Optional(f"{CONF_CHANNEL}_{i + 1}"): cv.Schema(
{
cv.Optional(CONF_CURRENT): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_CURRENT_AC,
accuracy_decimals=3,
device_class=DEVICE_CLASS_CURRENT,
unit_of_measurement=UNIT_AMPERE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_POWER): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
unit_of_measurement=UNIT_WATT,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_ENERGY): cv.maybe_simple_value(
sensor.sensor_schema(
icon=ICON_ENERGY,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
unit_of_measurement=UNIT_KILOWATT_HOURS,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
}
)
for i in range(6)
}
)
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("60s"))
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"bl0906", baud_rate=19200, require_tx=True, require_rx=True
)
@automation.register_action(
"bl0906.reset_energy",
ResetEnergyAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BL0906),
}
),
)
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)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_frequency_sensor(sens))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
for i in range(6):
if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"):
if current_config := channel_config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(getattr(var, f"set_current_{i + 1}_sensor")(sens))
if power_config := channel_config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(getattr(var, f"set_power_{i + 1}_sensor")(sens))
if energy_config := channel_config.get(CONF_ENERGY):
sens = await sensor.new_sensor(energy_config)
cg.add(getattr(var, f"set_energy_{i + 1}_sensor")(sens))
if total_power_config := config.get(CONF_TOTAL_POWER):
sens = await sensor.new_sensor(total_power_config)
cg.add(var.set_total_power_sensor(sens))
if total_energy_config := config.get(CONF_TOTAL_ENERGY):
sens = await sensor.new_sensor(total_energy_config)
cg.add(var.set_total_energy_sensor(sens))

View file

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

View file

@ -2,6 +2,8 @@
#include "esphome/core/log.h"
#include <cinttypes>
// Datasheet: https://www.belling.com.cn/media/file_object/bel_product/BL0942/datasheet/BL0942_V1.06_en.pdf
namespace esphome {
namespace bl0942 {
@ -12,43 +14,64 @@ static const uint8_t BL0942_FULL_PACKET = 0xAA;
static const uint8_t BL0942_PACKET_HEADER = 0x55;
static const uint8_t BL0942_WRITE_COMMAND = 0xA8;
static const uint8_t BL0942_REG_I_FAST_RMS_CTRL = 0x10;
static const uint8_t BL0942_REG_MODE = 0x18;
static const uint8_t BL0942_REG_SOFT_RESET = 0x19;
static const uint8_t BL0942_REG_USR_WRPROT = 0x1A;
static const uint8_t BL0942_REG_I_RMSOS = 0x12;
static const uint8_t BL0942_REG_WA_CREEP = 0x14;
static const uint8_t BL0942_REG_I_FAST_RMS_TH = 0x15;
static const uint8_t BL0942_REG_I_FAST_RMS_CYC = 0x16;
static const uint8_t BL0942_REG_FREQ_CYC = 0x17;
static const uint8_t BL0942_REG_OT_FUNX = 0x18;
static const uint8_t BL0942_REG_MODE = 0x19;
static const uint8_t BL0942_REG_SOFT_RESET = 0x1C;
static const uint8_t BL0942_REG_USR_WRPROT = 0x1D;
static const uint8_t BL0942_REG_TPS_CTRL = 0x1B;
// TODO: Confirm insialisation works as intended
const uint8_t BL0942_INIT[5][6] = {
// Reset to default
{BL0942_WRITE_COMMAND, BL0942_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38},
// Enable User Operation Write
{BL0942_WRITE_COMMAND, BL0942_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0},
// 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS
{BL0942_WRITE_COMMAND, BL0942_REG_MODE, 0x00, 0x10, 0x00, 0x37},
// 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS
{BL0942_WRITE_COMMAND, BL0942_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE},
// 0x181C = Half cycle, Fast RMS threshold 6172
{BL0942_WRITE_COMMAND, BL0942_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}};
static const uint32_t BL0942_REG_MODE_RESV = 0x03;
static const uint32_t BL0942_REG_MODE_CF_EN = 0x04;
static const uint32_t BL0942_REG_MODE_RMS_UPDATE_SEL = 0x08;
static const uint32_t BL0942_REG_MODE_FAST_RMS_SEL = 0x10;
static const uint32_t BL0942_REG_MODE_AC_FREQ_SEL = 0x20;
static const uint32_t BL0942_REG_MODE_CF_CNT_CLR_SEL = 0x40;
static const uint32_t BL0942_REG_MODE_CF_CNT_ADD_SEL = 0x80;
static const uint32_t BL0942_REG_MODE_UART_RATE_19200 = 0x200;
static const uint32_t BL0942_REG_MODE_UART_RATE_38400 = 0x300;
static const uint32_t BL0942_REG_MODE_DEFAULT =
BL0942_REG_MODE_RESV | BL0942_REG_MODE_CF_EN | BL0942_REG_MODE_CF_CNT_ADD_SEL;
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 (read_array((uint8_t *) &buffer, sizeof(buffer))) {
if (validate_checksum(&buffer)) {
received_package_(&buffer);
}
} else {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
while (read() >= 0)
;
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;
}
bool BL0942::validate_checksum(DataPacket *data) {
uint8_t checksum = BL0942_READ_COMMAND;
if (this->read_array((uint8_t *) &buffer, sizeof(buffer))) {
if (this->validate_checksum_(&buffer)) {
this->received_package_(&buffer);
}
}
this->rx_start_ = 0;
}
bool BL0942::validate_checksum_(DataPacket *data) {
uint8_t checksum = BL0942_READ_COMMAND | this->address_;
// Whole package but checksum
uint8_t *raw = (uint8_t *) data;
for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
@ -61,17 +84,73 @@ bool BL0942::validate_checksum(DataPacket *data) {
return checksum == data->checksum;
}
void BL0942::update() {
void BL0942::write_reg_(uint8_t reg, uint32_t val) {
uint8_t pkt[6];
this->flush();
this->write_byte(BL0942_READ_COMMAND);
pkt[0] = BL0942_WRITE_COMMAND | this->address_;
pkt[1] = reg;
pkt[2] = (val & 0xff);
pkt[3] = (val >> 8) & 0xff;
pkt[4] = (val >> 16) & 0xff;
pkt[5] = (pkt[0] + pkt[1] + pkt[2] + pkt[3] + pkt[4]) ^ 0xff;
this->write_array(pkt, 6);
delay(1);
}
int BL0942::read_reg_(uint8_t reg) {
union {
uint8_t b[4];
uint32_le_t le32;
} resp;
this->write_byte(BL0942_READ_COMMAND | this->address_);
this->write_byte(reg);
this->flush();
if (this->read_array(resp.b, 4) &&
resp.b[3] ==
(uint8_t) ((BL0942_READ_COMMAND + this->address_ + reg + resp.b[0] + resp.b[1] + resp.b[2]) ^ 0xff)) {
resp.b[3] = 0;
return resp.le32;
}
return -1;
}
void BL0942::update() {
this->write_byte(BL0942_READ_COMMAND | this->address_);
this->write_byte(BL0942_FULL_PACKET);
}
void BL0942::setup() {
for (auto *i : BL0942_INIT) {
this->write_array(i, 6);
delay(1);
// 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);
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 */
if (this->line_freq_ == LINE_FREQUENCY_60HZ)
mode |= BL0942_REG_MODE_AC_FREQ_SEL;
this->write_reg_(BL0942_REG_MODE, mode);
this->write_reg_(BL0942_REG_USR_WRPROT, 0);
if (this->read_reg_(BL0942_REG_MODE) != mode)
this->status_set_warning("BL0942 setup failed!");
this->flush();
}
@ -82,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;
@ -104,18 +190,25 @@ void BL0942::received_package_(DataPacket *data) {
if (frequency_sensor_ != nullptr) {
frequency_sensor_->publish_state(frequency);
}
this->status_clear_warning();
ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, frequency %fHz, status 0x%08X", v_rms, i_rms,
watt, cf_cnt, total_energy_consumption, frequency, data->status);
}
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
@ -28,6 +79,11 @@ struct DataPacket {
uint8_t checksum;
} __attribute__((packed));
enum LineFrequency : uint8_t {
LINE_FREQUENCY_50HZ = 50,
LINE_FREQUENCY_60HZ = 60,
};
class BL0942 : public PollingComponent, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
@ -35,9 +91,27 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
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;
void setup() override;
void dump_config() override;
@ -53,15 +127,25 @@ 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;
static bool validate_checksum(DataPacket *data);
bool validate_checksum_(DataPacket *data);
int read_reg_(uint8_t reg);
void write_reg_(uint8_t reg, uint32_t val);
void received_package_(DataPacket *data);
};
} // namespace bl0942

View file

@ -1,32 +1,46 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_CURRENT,
CONF_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_LINE_FREQUENCY,
CONF_POWER,
CONF_VOLTAGE,
CONF_FREQUENCY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_FREQUENCY,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HERTZ,
UNIT_KILOWATT_HOURS,
UNIT_VOLT,
UNIT_WATT,
UNIT_HERTZ,
STATE_CLASS_TOTAL_INCREASING,
)
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")
BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice)
LineFrequency = bl0942_ns.enum("LineFrequency")
LINE_FREQS = {
50: LineFrequency.LINE_FREQUENCY_50HZ,
60: LineFrequency.LINE_FREQUENCY_60HZ,
}
CONFIG_SCHEMA = (
cv.Schema(
{
@ -45,22 +59,35 @@ 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,
),
cv.Optional(CONF_LINE_FREQUENCY, default="50HZ"): cv.All(
cv.frequency,
cv.enum(
LINE_FREQS,
int=True,
),
),
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"))
@ -88,3 +115,14 @@ async def to_code(config):
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_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

@ -54,6 +54,9 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
}
resp.advertisements.push_back(std::move(adv));
ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
}
ESP_LOGV(TAG, "Proxying %d packets", count);
this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
@ -87,6 +90,8 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG, " Active: %s", YESNO(this->active_));
ESP_LOGCONFIG(TAG, " Connections: %d", this->connections_.size());
ESP_LOGCONFIG(TAG, " Raw advertisements: %s", YESNO(this->raw_advertisements_));
}
int BluetoothProxy::get_bluetooth_connections_free() {

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,4 +1,5 @@
#include "captive_portal.h"
#ifdef USE_CAPTIVE_PORTAL
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/wifi/wifi_component.h"
@ -91,3 +92,4 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo
} // namespace captive_portal
} // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_CAPTIVE_PORTAL
#include <memory>
#ifdef USE_ARDUINO
#include <DNSServer.h>
@ -71,3 +72,4 @@ extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-
} // namespace captive_portal
} // namespace esphome
#endif

View file

@ -0,0 +1,67 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_RESTORE_VALUE,
)
CODEOWNERS = ["@jesterret"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
ch422g_ns = cg.esphome_ns.namespace("ch422g")
CH422GComponent = ch422g_ns.class_("CH422GComponent", cg.Component, i2c.I2CDevice)
CH422GGPIOPin = ch422g_ns.class_(
"CH422GGPIOPin", cg.GPIOPin, cg.Parented.template(CH422GComponent)
)
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))
)
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)
CH422G_PIN_SCHEMA = pins.gpio_base_schema(
CH422GGPIOPin,
cv.int_range(min=0, max=7),
modes=[CONF_INPUT, CONF_OUTPUT],
).extend(
{
cv.Required(CONF_CH422G): cv.use_id(CH422GComponent),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA)
async def ch422g_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_CH422G])
cg.add(var.set_parent(parent))
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View file

@ -0,0 +1,122 @@
#include "ch422g.h"
#include "esphome/core/log.h"
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 char *const TAG = "ch422g";
void CH422GComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up CH422G...");
// Test to see if device exists
if (!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());
}
void CH422GComponent::loop() {
// Clear all the previously read flags.
this->pin_read_cache_ = 0x00;
}
void CH422GComponent::dump_config() {
ESP_LOGCONFIG(TAG, "CH422G:");
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with CH422G failed!");
}
}
// ch422g doesn't have any flag support (needs docs?)
void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {}
bool CH422GComponent::digital_read(uint8_t pin) {
if (this->pin_read_cache_ == 0 || this->pin_read_cache_ & (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);
}
void CH422GComponent::digital_write(uint8_t pin, bool value) {
if (value) {
this->write_output_(this->state_mask_ | (1 << pin));
} else {
this->write_output_(this->state_mask_ & ~(1 << pin));
}
}
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 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->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());
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;
}
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_; }
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_); }
} // namespace ch422g
} // namespace esphome

View file

@ -0,0 +1,70 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ch422g {
class CH422GComponent : public Component, public i2c::I2CDevice {
public:
CH422GComponent() = default;
/// Check i2c availability and setup masks
void setup() override;
/// Poll for input changes periodically
void loop() override;
/// Helper function to read the value of a pin.
bool digital_read(uint8_t pin);
/// Helper function to write the value of a pin.
void digital_write(uint8_t pin, bool value);
/// Helper function to set the pin mode of a pin.
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 read_inputs_();
bool write_output_(uint8_t value);
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t state_mask_{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};
};
/// Helper class to expose a CH422G pin as an internal input GPIO pin.
class CH422GGPIOPin : public GPIOPin {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
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; }
protected:
CH422GComponent *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace ch422g
} // namespace esphome

View file

@ -16,6 +16,8 @@
#include <esp32s2/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S3)
#include <esp32s3/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32H2)
#include <esp32h2/rom/rtc.h>
#endif
#ifdef USE_ARDUINO
#include <Esp.h>
@ -61,7 +63,7 @@ std::string DebugComponent::get_reset_reason_() {
case RTCWDT_SYS_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core";
break;
#if !defined(USE_ESP32_VARIANT_ESP32C6)
#if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
case INTRUSION_RESET:
reset_reason = "Intrusion Reset CPU";
break;

View file

@ -1,15 +1,15 @@
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import core, automation
from esphome.automation import maybe_simple_id
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_FROM,
CONF_ID,
CONF_LAMBDA,
CONF_PAGES,
CONF_PAGE_ID,
CONF_PAGES,
CONF_ROTATION,
CONF_FROM,
CONF_TO,
CONF_TRIGGER_ID,
)
@ -195,3 +195,4 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_global(display_ns.using)
cg.add_define("USE_DISPLAY")

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
#include "e131.h"
#ifdef USE_NETWORK
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"
@ -118,3 +119,4 @@ bool E131Component::process_(int universe, const E131Packet &packet) {
} // namespace e131
} // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include "esphome/components/socket/socket.h"
#include "esphome/core/component.h"
@ -53,3 +54,4 @@ class E131Component : public esphome::Component {
} // namespace e131
} // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#include "e131_addressable_light_effect.h"
#include "e131.h"
#ifdef USE_NETWORK
#include "esphome/core/log.h"
namespace esphome {
@ -90,3 +91,4 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
} // namespace e131
} // namespace esphome
#endif

View file

@ -2,7 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/components/light/addressable_light_effect.h"
#ifdef USE_NETWORK
namespace esphome {
namespace e131 {
@ -42,3 +42,4 @@ class E131AddressableLightEffect : public light::AddressableLightEffect {
} // namespace e131
} // namespace esphome
#endif

View file

@ -1,5 +1,6 @@
#include <cstring>
#include "e131.h"
#ifdef USE_NETWORK
#include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@ -137,3 +138,4 @@ bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E13
} // namespace e131
} // namespace esphome
#endif

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_) {

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,18 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
import esphome.codegen as cg
from esphome.components import canbus
from esphome.const import CONF_ID, CONF_RX_PIN, CONF_TX_PIN
from esphome.components.canbus import CanbusComponent, CanSpeed, CONF_BIT_RATE
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_RX_PIN,
CONF_RX_QUEUE_LEN,
CONF_TX_PIN,
CONF_TX_QUEUE_LEN,
)
CODEOWNERS = ["@Sympatron"]
@ -77,6 +82,8 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
cv.Optional(CONF_BIT_RATE, default="125KBPS"): validate_bit_rate,
cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_RX_QUEUE_LEN): cv.uint32_t,
cv.Optional(CONF_TX_QUEUE_LEN): cv.uint32_t,
}
)
@ -87,3 +94,7 @@ async def to_code(config):
cg.add(var.set_rx(config[CONF_RX_PIN]))
cg.add(var.set_tx(config[CONF_TX_PIN]))
if (rx_queue_len := config.get(CONF_RX_QUEUE_LEN)) is not None:
cg.add(var.set_rx_queue_len(rx_queue_len))
if (tx_queue_len := config.get(CONF_TX_QUEUE_LEN)) is not None:
cg.add(var.set_tx_queue_len(tx_queue_len))

View file

@ -69,6 +69,13 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
bool ESP32Can::setup_internal() {
twai_general_config_t g_config =
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
if (this->tx_queue_len_.has_value()) {
g_config.tx_queue_len = this->tx_queue_len_.value();
}
if (this->rx_queue_len_.has_value()) {
g_config.rx_queue_len = this->rx_queue_len_.value();
}
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
twai_timing_config_t t_config;
@ -111,6 +118,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
.flags = flags,
.identifier = frame->can_id,
.data_length_code = frame->can_data_length_code,
.data = {}, // to suppress warning, data is initialized properly below
};
if (!frame->remote_transmission_request) {
memcpy(message.data, frame->data, frame->can_data_length_code);

View file

@ -12,6 +12,8 @@ class ESP32Can : public canbus::Canbus {
public:
void set_rx(int rx) { rx_ = rx; }
void set_tx(int tx) { tx_ = tx; }
void set_tx_queue_len(uint32_t tx_queue_len) { this->tx_queue_len_ = tx_queue_len; }
void set_rx_queue_len(uint32_t rx_queue_len) { this->rx_queue_len_ = rx_queue_len; }
ESP32Can(){};
protected:
@ -21,6 +23,8 @@ class ESP32Can : public canbus::Canbus {
int rx_{-1};
int tx_{-1};
optional<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{};
};
} // namespace esp32_can

View file

@ -38,7 +38,8 @@ void ESP32RMTLEDStripLightOutput::setup() {
}
ExternalRAMAllocator<rmt_item32_t> rmt_allocator(ExternalRAMAllocator<rmt_item32_t>::ALLOW_FAILURE);
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 +
1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
rmt_config_t config;
memset(&config, 0, sizeof(config));
@ -66,7 +67,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
}
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low) {
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
// 0-bit
@ -79,6 +80,11 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
this->bit1_.level0 = 1;
this->bit1_.duration1 = (uint32_t) (ratio * bit1_low);
this->bit1_.level1 = 0;
// reset
this->reset_.duration0 = (uint32_t) (ratio * reset_time_high);
this->reset_.level0 = 1;
this->reset_.duration1 = (uint32_t) (ratio * reset_time_low);
this->reset_.level1 = 0;
}
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
@ -118,6 +124,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
psrc++;
}
if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) {
pdest->val = this->reset_.val;
pdest++;
len++;
}
if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) {
ESP_LOGE(TAG, "RMT TX error");
this->status_set_warning();

View file

@ -49,7 +49,8 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low);
void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low,
uint32_t reset_time_high, uint32_t reset_time_low);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
@ -75,7 +76,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
bool is_rgbw_;
bool is_wrgb_;
rmt_item32_t bit0_, bit1_;
rmt_item32_t bit0_, bit1_, reset_;
RGBOrder rgb_order_;
rmt_channel_t channel_;

View file

@ -43,13 +43,16 @@ class LEDStripTimings:
bit0_low: int
bit1_high: int
bit1_low: int
reset_high: int
reset_low: int
CHIPSETS = {
"WS2812": LEDStripTimings(400, 1000, 1000, 400),
"SK6812": LEDStripTimings(300, 900, 600, 600),
"APA106": LEDStripTimings(350, 1360, 1360, 350),
"SM16703": LEDStripTimings(300, 900, 900, 300),
"WS2811": LEDStripTimings(300, 1090, 1090, 320, 0, 300000),
"WS2812": LEDStripTimings(400, 1000, 1000, 400, 0, 0),
"SK6812": LEDStripTimings(300, 900, 600, 600, 0, 0),
"APA106": LEDStripTimings(350, 1360, 1360, 350, 0, 0),
"SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0),
}
@ -58,6 +61,8 @@ CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high"
CONF_BIT1_LOW = "bit1_low"
CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low"
CONFIG_SCHEMA = cv.All(
@ -88,6 +93,14 @@ CONFIG_SCHEMA = cv.All(
CONF_BIT1_LOW,
"custom",
): cv.positive_time_period_nanoseconds,
cv.Optional(
CONF_RESET_HIGH,
default="0 us",
): cv.positive_time_period_nanoseconds,
cv.Optional(
CONF_RESET_LOW,
default="0 us",
): cv.positive_time_period_nanoseconds,
}
),
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
@ -113,6 +126,8 @@ async def to_code(config):
chipset.bit0_low,
chipset.bit1_high,
chipset.bit1_low,
chipset.reset_high,
chipset.reset_low,
)
)
else:
@ -122,6 +137,8 @@ async def to_code(config):
config[CONF_BIT0_LOW],
config[CONF_BIT1_HIGH],
config[CONF_BIT1_LOW],
config[CONF_RESET_HIGH],
config[CONF_RESET_LOW],
)
)

View file

@ -1,5 +1,5 @@
#include "ota_esphome.h"
#ifdef USE_OTA
#include "esphome/components/md5/md5.h"
#include "esphome/components/network/util.h"
#include "esphome/components/ota/ota_backend.h"
@ -410,3 +410,4 @@ float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::A
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
} // namespace esphome
#endif

View file

@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_OTA
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/components/ota/ota_backend.h"
@ -41,3 +42,4 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
};
} // namespace esphome
#endif

View file

@ -1,43 +1,35 @@
import functools
import hashlib
import logging
import functools
from pathlib import Path
import os
from pathlib import Path
import re
from packaging import version
import requests
from esphome import core
from esphome import external_files
import esphome.config_validation as cv
from esphome import core, external_files
import esphome.codegen as cg
from esphome.helpers import (
copy_file_if_changed,
cpp_string_escape,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_FAMILY,
CONF_FILE,
CONF_GLYPHS,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_REFRESH,
CONF_SIZE,
CONF_PATH,
CONF_WEIGHT,
CONF_TYPE,
CONF_URL,
CONF_WEIGHT,
)
from esphome.core import (
CORE,
HexInt,
)
from esphome.core import CORE, HexInt
from esphome.helpers import copy_file_if_changed, cpp_string_escape
_LOGGER = logging.getLogger(__name__)
DOMAIN = "font"
DEPENDENCIES = ["display"]
MULTI_CONF = True
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
@ -400,10 +392,7 @@ class EFont:
def convert_bitmap_to_pillow_font(filepath):
from PIL import (
PcfFontFile,
BdfFontFile,
)
from PIL import BdfFontFile, PcfFontFile
local_bitmap_font_file = external_files.compute_local_file_dir(
DOMAIN,

View file

@ -1,9 +1,8 @@
#include "font.h"
#include "esphome/core/color.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/color.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome {
namespace font {
@ -68,6 +67,7 @@ int Font::match_next_glyph(const uint8_t *str, int *match_length) {
return -1;
return lo;
}
#ifdef USE_DISPLAY
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
@ -164,6 +164,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
i += match_length;
}
}
#endif
} // namespace font
} // namespace esphome

View file

@ -1,8 +1,11 @@
#pragma once
#include "esphome/core/datatypes.h"
#include "esphome/core/color.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/datatypes.h"
#include "esphome/core/defines.h"
#ifdef USE_DISPLAY
#include "esphome/components/display/display.h"
#endif
namespace esphome {
namespace font {
@ -38,7 +41,11 @@ class Glyph {
const GlyphData *glyph_data_;
};
class Font : public display::BaseFont {
class Font
#ifdef USE_DISPLAY
: public display::BaseFont
#endif
{
public:
/** Construct the font with the given glyphs.
*
@ -50,9 +57,11 @@ class Font : public display::BaseFont {
int match_next_glyph(const uint8_t *str, int *match_length);
#ifdef USE_DISPLAY
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
Color background) override;
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override;
#endif
inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; }
inline int get_bpp() { return this->bpp_; }

View file

@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODEL
CODEOWNERS = ["@orestismers"]
@ -17,6 +17,7 @@ MODELS = {
"yaa": Model.GREE_YAA,
"yac": Model.GREE_YAC,
"yac1fb9": Model.GREE_YAC1FB9,
"yx1ff": Model.GREE_YX1FF,
}
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(

View file

@ -6,7 +6,15 @@ namespace gree {
static const char *const TAG = "gree.climate";
void GreeClimate::set_model(Model model) { this->model_ = model; }
void GreeClimate::set_model(Model model) {
if (model == GREE_YX1FF) {
this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed
this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode
this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode
}
this->model_ = model;
}
void GreeClimate::transmit_state() {
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
@ -14,7 +22,7 @@ void GreeClimate::transmit_state() {
remote_state[0] = this->fan_speed_() | this->operation_mode_();
remote_state[1] = this->temperature_();
if (this->model_ == GREE_YAN) {
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
remote_state[2] = 0x60;
remote_state[3] = 0x50;
remote_state[4] = this->vertical_swing_();
@ -36,8 +44,18 @@ void GreeClimate::transmit_state() {
}
}
if (this->model_ == GREE_YX1FF) {
if (this->fan_speed_() == GREE_FAN_TURBO) {
remote_state[2] |= GREE_FAN_TURBO_BIT;
}
if (this->preset_() == GREE_PRESET_SLEEP) {
remote_state[0] |= GREE_PRESET_SLEEP_BIT;
}
}
// Calculate the checksum
if (this->model_ == GREE_YAN) {
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else {
remote_state[7] =
@ -124,6 +142,23 @@ uint8_t GreeClimate::operation_mode_() {
}
uint8_t GreeClimate::fan_speed_() {
// YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high
if (this->model_ == GREE_YX1FF) {
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_QUIET:
return GREE_FAN_1;
case climate::CLIMATE_FAN_LOW:
return GREE_FAN_2;
case climate::CLIMATE_FAN_MEDIUM:
return GREE_FAN_3;
case climate::CLIMATE_FAN_HIGH:
return GREE_FAN_TURBO;
case climate::CLIMATE_FAN_AUTO:
default:
return GREE_FAN_AUTO;
}
}
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_LOW:
return GREE_FAN_1;
@ -161,5 +196,21 @@ uint8_t GreeClimate::temperature_() {
return (uint8_t) roundf(clamp<float>(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX));
}
uint8_t GreeClimate::preset_() {
// YX1FF has sleep preset
if (this->model_ == GREE_YX1FF) {
switch (this->preset.value()) {
case climate::CLIMATE_PRESET_NONE:
return GREE_PRESET_NONE;
case climate::CLIMATE_PRESET_SLEEP:
return GREE_PRESET_SLEEP;
default:
return GREE_PRESET_NONE;
}
}
return GREE_PRESET_NONE;
}
} // namespace gree
} // namespace esphome

View file

@ -25,7 +25,6 @@ const uint8_t GREE_FAN_AUTO = 0x00;
const uint8_t GREE_FAN_1 = 0x10;
const uint8_t GREE_FAN_2 = 0x20;
const uint8_t GREE_FAN_3 = 0x30;
const uint8_t GREE_FAN_TURBO = 0x80;
// IR Transmission
const uint32_t GREE_IR_FREQUENCY = 38000;
@ -70,8 +69,16 @@ const uint8_t GREE_HDIR_MIDDLE = 0x04;
const uint8_t GREE_HDIR_MRIGHT = 0x05;
const uint8_t GREE_HDIR_RIGHT = 0x06;
// Only available on YX1FF
// Turbo (high) fan mode + sleep preset mode
const uint8_t GREE_FAN_TURBO = 0x80;
const uint8_t GREE_FAN_TURBO_BIT = 0x10;
const uint8_t GREE_PRESET_NONE = 0x00;
const uint8_t GREE_PRESET_SLEEP = 0x01;
const uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Model codes
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9 };
enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF };
class GreeClimate : public climate_ir::ClimateIR {
public:
@ -93,6 +100,7 @@ class GreeClimate : public climate_ir::ClimateIR {
uint8_t horizontal_swing_();
uint8_t vertical_swing_();
uint8_t temperature_();
uint8_t preset_();
Model model_{};
};

View file

@ -1,11 +1,10 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
from esphome.const import CONF_INTERRUPT_PIN, CONF_ID
from .. import gt911_ns
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from .. import gt911_ns
GT911ButtonListener = gt911_ns.class_("GT911ButtonListener")
GT911Touchscreen = gt911_ns.class_(
@ -18,6 +17,7 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GT911Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
).extend(i2c.i2c_device_schema(0x5D))
@ -29,3 +29,5 @@ async def to_code(config):
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
if reset_pin := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))

View file

@ -26,6 +26,23 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
void GT911Touchscreen::setup() {
i2c::ErrorCode err;
ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen...");
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
if (this->interrupt_pin_ != nullptr) {
// The interrupt pin is used as an input during reset to select the I2C address.
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->interrupt_pin_->setup();
this->interrupt_pin_->digital_write(false);
}
delay(2);
this->reset_pin_->digital_write(true);
delay(50); // NOLINT
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
}
}
// check the configuration of the int line.
uint8_t data[4];

View file

@ -19,12 +19,14 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); }
protected:
void update_touches() override;
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
std::vector<GT911ButtonListener *> button_listeners_;
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
};

View file

@ -0,0 +1,2 @@
AUTO_LOAD = ["md5"]
CODEOWNERS = ["@dwmw2"]

View file

@ -0,0 +1,56 @@
#include <cstdio>
#include <cstring>
#include "hmac_md5.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace hmac_md5 {
void HmacMD5::init(const uint8_t *key, size_t len) {
uint8_t ipad[64], opad[64];
memset(ipad, 0, sizeof(ipad));
if (len > 64) {
md5::MD5Digest keymd5;
keymd5.init();
keymd5.add(key, len);
keymd5.calculate();
keymd5.get_bytes(ipad);
} else {
memcpy(ipad, key, len);
}
memcpy(opad, ipad, sizeof(opad));
for (int i = 0; i < 64; i++) {
ipad[i] ^= 0x36;
opad[i] ^= 0x5c;
}
this->ihash_.init();
this->ihash_.add(ipad, sizeof(ipad));
this->ohash_.init();
this->ohash_.add(opad, sizeof(opad));
}
void HmacMD5::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); }
void HmacMD5::calculate() {
uint8_t ibytes[16];
this->ihash_.calculate();
this->ihash_.get_bytes(ibytes);
this->ohash_.add(ibytes, sizeof(ibytes));
this->ohash_.calculate();
}
void HmacMD5::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); }
void HmacMD5::get_hex(char *output) { this->ohash_.get_hex(output); }
bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); }
bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); }
} // namespace hmac_md5
} // namespace esphome

View file

@ -0,0 +1,48 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/components/md5/md5.h"
#include <string>
namespace esphome {
namespace hmac_md5 {
class HmacMD5 {
public:
HmacMD5() = default;
~HmacMD5() = default;
/// Initialize a new MD5 digest computation.
void init(const uint8_t *key, size_t len);
void init(const char *key, size_t len) { this->init((const uint8_t *) key, len); }
void init(const std::string &key) { this->init(key.c_str(), key.length()); }
/// Add bytes of data for the digest.
void add(const uint8_t *data, size_t len);
void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); }
/// Compute the digest, based on the provided data.
void calculate();
/// Retrieve the HMAC-MD5 digest as bytes.
/// The output must be able to hold 16 bytes or more.
void get_bytes(uint8_t *output);
/// Retrieve the HMAC-MD5 digest as hex characters.
/// The output must be able to hold 32 bytes or more.
void get_hex(char *output);
/// Compare the digest against a provided byte-encoded digest (16 bytes).
bool equals_bytes(const uint8_t *expected);
/// Compare the digest against a provided hex-encoded digest (32 bytes).
bool equals_hex(const char *expected);
protected:
md5::MD5Digest ihash_;
md5::MD5Digest ohash_;
};
} // namespace hmac_md5
} // 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,19 +167,22 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) {
return 0;
}
this->status_clear_warning();
if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) {
// ESP-IDF I2S implementation right-extends 8-bit data to 16 bits,
// and 24-bit data to 32 bits.
switch (this->bits_per_sample_) {
case I2S_BITS_PER_SAMPLE_8BIT:
case I2S_BITS_PER_SAMPLE_16BIT:
return bytes_read;
} else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) {
std::vector<int16_t> samples;
case I2S_BITS_PER_SAMPLE_24BIT:
case I2S_BITS_PER_SAMPLE_32BIT: {
size_t samples_read = bytes_read / sizeof(int32_t);
samples.resize(samples_read);
for (size_t i = 0; i < samples_read; i++) {
int32_t temp = reinterpret_cast<int32_t *>(buf)[i] >> 14;
samples[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
buf[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX);
}
memcpy(buf, samples.data(), samples_read * sizeof(int16_t));
return samples_read * sizeof(int16_t);
} else {
}
default:
ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_);
return 0;
}

View file

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

View file

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

View file

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

View file

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

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