Merge branch 'dev' into dev

This commit is contained in:
optimusprimespace 2024-06-18 16:08:43 +02:00 committed by GitHub
commit 82f2396743
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1891 changed files with 4997 additions and 3352 deletions

View file

@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v6.0.0
with:
context: .
file: ./docker/Dockerfile
@ -69,7 +69,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v6.0.0
with:
context: .
file: ./docker/Dockerfile

View file

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
with:

View file

@ -40,7 +40,7 @@ jobs:
arch: [amd64, armv7, aarch64]
build_type: ["ha-addon", "docker", "lint"]
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
with:

View file

@ -34,7 +34,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
@ -66,7 +66,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -87,7 +87,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -108,7 +108,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -129,7 +129,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -150,7 +150,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -199,7 +199,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -229,7 +229,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -254,7 +254,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Find all YAML test files
id: set-matrix
run: echo "matrix=$(ls tests/test*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
@ -271,7 +271,7 @@ jobs:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -303,7 +303,7 @@ jobs:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -358,7 +358,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -387,6 +387,13 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Run 'pio run --list-targets -e esp32-idf-tidy'
if: matrix.name == 'Run script/clang-tidy for ESP32 IDF'
run: |
. venv/bin/activate
mkdir -p .temp
pio run --list-targets -e esp32-idf-tidy
- name: Run clang-tidy
run: |
. venv/bin/activate
@ -410,7 +417,7 @@ jobs:
count: ${{ steps.list-components.outputs.count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
@ -454,11 +461,11 @@ jobs:
matrix:
file: ${{ fromJson(needs.list-components.outputs.components) }}
steps:
- name: Install libsodium
run: sudo apt-get install libsodium-dev
- name: Install dependencies
run: sudo apt-get install libsodium-dev libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -484,7 +491,7 @@ jobs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Split components into 20 groups
id: split
run: |
@ -508,11 +515,11 @@ jobs:
- name: List components
run: echo ${{ matrix.components }}
- name: Install libsodium
run: sudo apt-get install libsodium-dev
- name: Install dependencies
run: sudo apt-get install libsodium-dev libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:

View file

@ -19,7 +19,7 @@ jobs:
tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Get tag
id: tag
# yamllint disable rule:line-length
@ -51,7 +51,7 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
with:
@ -65,7 +65,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.8.14
uses: pypa/gh-action-pypi-publish@v1.9.0
deploy-docker:
name: Build ESPHome ${{ matrix.platform }}
@ -83,7 +83,7 @@ jobs:
- linux/arm/v7
- linux/arm64
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.1.0
with:
@ -174,7 +174,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Download digests
uses: actions/download-artifact@v4.1.7

View file

@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Checkout Home Assistant
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
with:
repository: home-assistant/core
path: lib/home-assistant

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Run yamllint
uses: frenck/action-yamllint@v1.5.0
with:

View file

@ -94,6 +94,7 @@ esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
esphome/components/dallas_temp/* @ssieb
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
@ -144,6 +145,7 @@ esphome/components/gdk101/* @Szewcson
esphome/components/globals/* @esphome/core
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
@ -172,6 +174,7 @@ esphome/components/host/time/* @clydebarrow
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
@ -269,6 +272,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931
@ -316,6 +320,7 @@ esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core
esphome/components/sdl/* @clydebarrow
esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath
esphome/components/seeed_mr24hpc1/* @limengdu
@ -410,6 +415,7 @@ esphome/components/uart/button/* @ssieb
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/valve/* @esphome/core
esphome/components/vbus/* @ssieb

View file

@ -81,7 +81,8 @@ RUN \
fi; \
pip3 install \
--break-system-packages --no-cache-dir \
platformio==6.1.13 \
# Keep platformio version in sync with requirements.txt
platformio==6.1.15 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \
@ -101,7 +102,7 @@ RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "a
&& /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 '/config/*'
RUN git config --system --add safe.directory '*'
# ======================= docker-type image =======================

View file

@ -488,6 +488,15 @@ def command_run(args, config):
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
from esphome.platformio_api import get_idedata
idedata = get_idedata(config)
if idedata is None:
return 1
program_path = idedata.raw["prog_path"]
return run_external_process(program_path)
port = choose_upload_log_host(
default=args.device,
check_default=None,

View file

@ -58,6 +58,7 @@ from esphome.cpp_types import ( # noqa
bool_,
int_,
std_ns,
std_shared_ptr,
std_string,
std_vector,
uint8,

View file

@ -3,7 +3,13 @@ import logging
from esphome import automation, core
from esphome.components import font
import esphome.components.image as espImage
from esphome.components.image import CONF_USE_TRANSPARENCY
from esphome.components.image import (
CONF_USE_TRANSPARENCY,
LOCAL_SCHEMA,
WEB_SCHEMA,
SOURCE_WEB,
SOURCE_LOCAL,
)
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
@ -13,6 +19,9 @@ from esphome.const import (
CONF_REPEAT,
CONF_RESIZE,
CONF_TYPE,
CONF_SOURCE,
CONF_PATH,
CONF_URL,
)
from esphome.core import CORE, HexInt
@ -43,6 +52,40 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
},
key=CONF_SOURCE,
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
def validate_cross_dependencies(config):
"""
@ -67,7 +110,7 @@ ANIMATION_SCHEMA = cv.Schema(
cv.All(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): cv.file_,
cv.Required(CONF_FILE): FILE_SCHEMA,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True
@ -124,7 +167,11 @@ async def animation_action_to_code(config, action_id, template_arg, args):
async def to_code(config):
from PIL import Image
path = CORE.relative_config_path(config[CONF_FILE])
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = espImage.compute_local_image_path(conf_file).as_posix()
try:
image = Image.open(path)
except Exception as e:

View file

@ -48,6 +48,7 @@ service APIConnection {
rpc date_command (DateCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc datetime_command (DateTimeCommandRequest) returns (void) {}
rpc update_command (UpdateCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@ -1837,3 +1838,46 @@ message DateTimeCommandRequest {
fixed32 key = 1;
fixed32 epoch_seconds = 2;
}
// ==================== UPDATE ====================
message ListEntitiesUpdateResponse {
option (id) = 116;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
}
message UpdateStateResponse {
option (id) = 117;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
fixed32 key = 1;
bool missing_state = 2;
bool in_progress = 3;
bool has_progress = 4;
float progress = 5;
string current_version = 6;
string latest_version = 7;
string title = 8;
string release_summary = 9;
string release_url = 10;
}
message UpdateCommandRequest {
option (id) = 118;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
fixed32 key = 1;
bool install = 2;
}

View file

@ -1287,6 +1287,51 @@ bool APIConnection::send_event_info(event::Event *event) {
}
#endif
#ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) {
if (!this->state_subscription_)
return false;
UpdateStateResponse resp{};
resp.key = update->get_object_id_hash();
resp.missing_state = !update->has_state();
if (update->has_state()) {
resp.in_progress = update->state == update::UpdateState::UPDATE_STATE_INSTALLING;
if (update->update_info.has_progress) {
resp.has_progress = true;
resp.progress = update->update_info.progress;
}
resp.current_version = update->update_info.current_version;
resp.latest_version = update->update_info.latest_version;
resp.title = update->update_info.title;
resp.release_summary = update->update_info.summary;
resp.release_url = update->update_info.release_url;
}
return this->send_update_state_response(resp);
}
bool APIConnection::send_update_info(update::UpdateEntity *update) {
ListEntitiesUpdateResponse msg;
msg.key = update->get_object_id_hash();
msg.object_id = update->get_object_id();
if (update->has_own_name())
msg.name = update->get_name();
msg.unique_id = get_default_unique_id("update", update);
msg.icon = update->get_icon();
msg.disabled_by_default = update->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(update->get_entity_category());
msg.device_class = update->get_device_class();
return this->send_list_entities_update_response(msg);
}
void APIConnection::update_command(const UpdateCommandRequest &msg) {
update::UpdateEntity *update = App.get_update_by_key(msg.key);
if (update == nullptr)
return;
update->perform();
}
#endif
bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
if (this->log_subscription_ < level)
return false;

View file

@ -164,6 +164,12 @@ class APIConnection : public APIServerConnection {
bool send_event_info(event::Event *event);
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
bool send_update_info(update::UpdateEntity *update);
void update_command(const UpdateCommandRequest &msg) override;
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping

View file

@ -8376,6 +8376,262 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
default:
return false;
}
}
bool ListEntitiesUpdateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
case 8: {
this->device_class = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesUpdateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_string(8, this->device_class);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesUpdateResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append("}");
}
#endif
bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->missing_state = value.as_bool();
return true;
}
case 3: {
this->in_progress = value.as_bool();
return true;
}
case 4: {
this->has_progress = value.as_bool();
return true;
}
default:
return false;
}
}
bool UpdateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 6: {
this->current_version = value.as_string();
return true;
}
case 7: {
this->latest_version = value.as_string();
return true;
}
case 8: {
this->title = value.as_string();
return true;
}
case 9: {
this->release_summary = value.as_string();
return true;
}
case 10: {
this->release_url = value.as_string();
return true;
}
default:
return false;
}
}
bool UpdateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 5: {
this->progress = value.as_float();
return true;
}
default:
return false;
}
}
void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->missing_state);
buffer.encode_bool(3, this->in_progress);
buffer.encode_bool(4, this->has_progress);
buffer.encode_float(5, this->progress);
buffer.encode_string(6, this->current_version);
buffer.encode_string(7, this->latest_version);
buffer.encode_string(8, this->title);
buffer.encode_string(9, this->release_summary);
buffer.encode_string(10, this->release_url);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void UpdateStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("UpdateStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" missing_state: ");
out.append(YESNO(this->missing_state));
out.append("\n");
out.append(" in_progress: ");
out.append(YESNO(this->in_progress));
out.append("\n");
out.append(" has_progress: ");
out.append(YESNO(this->has_progress));
out.append("\n");
out.append(" progress: ");
sprintf(buffer, "%g", this->progress);
out.append(buffer);
out.append("\n");
out.append(" current_version: ");
out.append("'").append(this->current_version).append("'");
out.append("\n");
out.append(" latest_version: ");
out.append("'").append(this->latest_version).append("'");
out.append("\n");
out.append(" title: ");
out.append("'").append(this->title).append("'");
out.append("\n");
out.append(" release_summary: ");
out.append("'").append(this->release_summary).append("'");
out.append("\n");
out.append(" release_url: ");
out.append("'").append(this->release_url).append("'");
out.append("\n");
out.append("}");
}
#endif
bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->install = value.as_bool();
return true;
}
default:
return false;
}
}
bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->install);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void UpdateCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("UpdateCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" install: ");
out.append(YESNO(this->install));
out.append("\n");
out.append("}");
}
#endif
} // namespace api
} // namespace esphome

View file

@ -2130,6 +2130,61 @@ class DateTimeCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class ListEntitiesUpdateResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class UpdateStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
bool in_progress{false};
bool has_progress{false};
float progress{0.0f};
std::string current_version{};
std::string latest_version{};
std::string title{};
std::string release_summary{};
std::string release_url{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class UpdateCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
bool install{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
} // namespace api
} // namespace esphome

View file

@ -611,6 +611,24 @@ bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateR
#endif
#ifdef USE_DATETIME_DATETIME
#endif
#ifdef USE_UPDATE
bool APIServerConnectionBase::send_list_entities_update_response(const ListEntitiesUpdateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_update_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesUpdateResponse>(msg, 116);
}
#endif
#ifdef USE_UPDATE
bool APIServerConnectionBase::send_update_state_response(const UpdateStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_update_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<UpdateStateResponse>(msg, 117);
}
#endif
#ifdef USE_UPDATE
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) {
case 1: {
@ -1106,6 +1124,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str());
#endif
this->on_voice_assistant_timer_event_response(msg);
#endif
break;
}
case 118: {
#ifdef USE_UPDATE
UpdateCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str());
#endif
this->on_update_command_request(msg);
#endif
break;
}
@ -1434,6 +1463,19 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
this->datetime_command(msg);
}
#endif
#ifdef USE_UPDATE
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->update_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {

View file

@ -306,6 +306,15 @@ class APIServerConnectionBase : public ProtoService {
#endif
#ifdef USE_DATETIME_DATETIME
virtual void on_date_time_command_request(const DateTimeCommandRequest &value){};
#endif
#ifdef USE_UPDATE
bool send_list_entities_update_response(const ListEntitiesUpdateResponse &msg);
#endif
#ifdef USE_UPDATE
bool send_update_state_response(const UpdateStateResponse &msg);
#endif
#ifdef USE_UPDATE
virtual void on_update_command_request(const UpdateCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@ -373,6 +382,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_DATETIME_DATETIME
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
#endif
#ifdef USE_UPDATE
virtual void update_command(const UpdateCommandRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif
@ -471,6 +483,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_DATETIME_DATETIME
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_UPDATE
void on_update_command_request(const UpdateCommandRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif

View file

@ -334,6 +334,13 @@ void APIServer::on_event(event::Event *obj, const std::string &event_type) {
}
#endif
#ifdef USE_UPDATE
void APIServer::on_update(update::UpdateEntity *obj) {
for (auto &c : this->clients_)
c->send_update_state(obj);
}
#endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View file

@ -102,6 +102,9 @@ class APIServer : public Component, public Controller {
#ifdef USE_EVENT
void on_event(event::Event *obj, const std::string &event_type) override;
#endif
#ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override;
#endif
bool is_connected() const;

View file

@ -98,6 +98,9 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
#ifdef USE_EVENT
bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); }
#endif
#ifdef USE_UPDATE
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); }
#endif
} // namespace api
} // namespace esphome

View file

@ -75,6 +75,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override;
#endif
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override;
#endif
bool on_end() override;

View file

@ -77,6 +77,9 @@ bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
}
#endif
#ifdef USE_UPDATE
bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); }
#endif
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api

View file

@ -72,6 +72,9 @@ class InitialStateIterator : public ComponentIterator {
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override;
#endif
protected:
APIConnection *client_;

View file

@ -6,18 +6,24 @@ namespace climate_ir_lg {
static const char *const TAG = "climate.climate_ir_lg";
const uint32_t COMMAND_ON = 0x00000;
const uint32_t COMMAND_ON_AI = 0x03000;
const uint32_t COMMAND_COOL = 0x08000;
const uint32_t COMMAND_HEAT = 0x0C000;
// Commands
const uint32_t COMMAND_MASK = 0xFF000;
const uint32_t COMMAND_OFF = 0xC0000;
const uint32_t COMMAND_SWING = 0x10000;
// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore.
const uint32_t COMMAND_AUTO = 0x0B000;
const uint32_t COMMAND_DRY_FAN = 0x09000;
const uint32_t COMMAND_MASK = 0xFF000;
const uint32_t COMMAND_ON_COOL = 0x00000;
const uint32_t COMMAND_ON_DRY = 0x01000;
const uint32_t COMMAND_ON_FAN_ONLY = 0x02000;
const uint32_t COMMAND_ON_AI = 0x03000;
const uint32_t COMMAND_ON_HEAT = 0x04000;
const uint32_t COMMAND_COOL = 0x08000;
const uint32_t COMMAND_DRY = 0x09000;
const uint32_t COMMAND_FAN_ONLY = 0x0A000;
const uint32_t COMMAND_AI = 0x0B000;
const uint32_t COMMAND_HEAT = 0x0C000;
// Fan speed
const uint32_t FAN_MASK = 0xF0;
const uint32_t FAN_AUTO = 0x50;
const uint32_t FAN_MIN = 0x00;
@ -35,28 +41,28 @@ void LgIrClimate::transmit_state() {
uint32_t remote_state = 0x8800000;
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
// Set command
if (send_swing_cmd_) {
send_swing_cmd_ = false;
remote_state |= COMMAND_SWING;
} else {
if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
remote_state |= COMMAND_ON_AI;
} else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) {
remote_state |= COMMAND_ON;
this->mode = climate::CLIMATE_MODE_COOL;
} else {
bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF);
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
remote_state |= COMMAND_COOL;
break;
case climate::CLIMATE_MODE_HEAT:
remote_state |= COMMAND_HEAT;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
remote_state |= COMMAND_AUTO;
remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
break;
case climate::CLIMATE_MODE_DRY:
remote_state |= COMMAND_DRY_FAN;
remote_state |= climate_is_off ? COMMAND_ON_DRY : COMMAND_DRY;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
remote_state |= climate_is_off ? COMMAND_ON_FAN_ONLY : COMMAND_FAN_ONLY;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
remote_state |= climate_is_off ? COMMAND_ON_AI : COMMAND_AI;
break;
case climate::CLIMATE_MODE_HEAT:
remote_state |= climate_is_off ? COMMAND_ON_HEAT : COMMAND_HEAT;
break;
case climate::CLIMATE_MODE_OFF:
default:
@ -64,14 +70,15 @@ void LgIrClimate::transmit_state() {
break;
}
}
mode_before_ = this->mode;
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
// Set fan speed
if (this->mode == climate::CLIMATE_MODE_OFF) {
remote_state |= FAN_AUTO;
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
this->mode == climate::CLIMATE_MODE_HEAT) {
} else {
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_HIGH:
remote_state |= FAN_MAX;
@ -89,15 +96,12 @@ void LgIrClimate::transmit_state() {
}
}
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
// remote_state |= FAN_MODE_AUTO_DRY;
}
// Set temperature
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
auto temp = (uint8_t) roundf(clamp<float>(this->target_temperature, TEMP_MIN, TEMP_MAX));
remote_state |= ((temp - 15) << TEMP_SHIFT);
}
}
transmit_(remote_state);
this->publish_state();
}
@ -125,37 +129,42 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
if ((remote_state & 0xFF00000) != 0x8800000)
return false;
if ((remote_state & COMMAND_MASK) == COMMAND_ON) {
this->mode = climate::CLIMATE_MODE_COOL;
} else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
}
// Get command
if ((remote_state & COMMAND_MASK) == COMMAND_OFF) {
this->mode = climate::CLIMATE_MODE_OFF;
} else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) {
this->swing_mode =
this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
} else {
if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
} else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) {
switch (remote_state & COMMAND_MASK) {
case COMMAND_DRY:
case COMMAND_ON_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
} else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) {
break;
case COMMAND_FAN_ONLY:
case COMMAND_ON_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case COMMAND_AI:
case COMMAND_ON_AI:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case COMMAND_HEAT:
case COMMAND_ON_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
} else {
break;
case COMMAND_COOL:
case COMMAND_ON_COOL:
default:
this->mode = climate::CLIMATE_MODE_COOL;
break;
}
// Temperature
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT)
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
// Fan Speed
// Get fan speed
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT ||
this->mode == climate::CLIMATE_MODE_DRY) {
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_HEAT) {
if ((remote_state & FAN_MASK) == FAN_AUTO) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
} else if ((remote_state & FAN_MASK) == FAN_MIN) {
@ -166,11 +175,17 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
this->fan_mode = climate::CLIMATE_FAN_HIGH;
}
}
// Get temperature
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
}
}
this->publish_state();
return true;
}
void LgIrClimate::transmit_(uint32_t value) {
calc_checksum_(value);
ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value);

View file

@ -14,7 +14,7 @@ const uint8_t TEMP_MAX = 30; // Celsius
class LgIrClimate : public climate_ir::ClimateIR {
public:
LgIrClimate()
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false,
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}

View file

@ -1,25 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_PIN
MULTI_CONF = True
AUTO_LOAD = ["sensor"]
dallas_ns = cg.esphome_ns.namespace("dallas")
DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DallasComponent),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
}
).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
CONFIG_SCHEMA = cv.invalid(
'The "dallas" component has been replaced by the "one_wire" component.\nhttps://esphome.io/components/one_wire'
)

View file

@ -1,287 +0,0 @@
#include "dallas_component.h"
#include "esphome/core/log.h"
namespace esphome {
namespace dallas {
static const char *const TAG = "dallas.sensor";
static const uint8_t DALLAS_MODEL_DS18S20 = 0x10;
static const uint8_t DALLAS_MODEL_DS1822 = 0x22;
static const uint8_t DALLAS_MODEL_DS18B20 = 0x28;
static const uint8_t DALLAS_MODEL_DS1825 = 0x3B;
static const uint8_t DALLAS_MODEL_DS28EA00 = 0x42;
static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44;
static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE;
static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E;
uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion() const {
switch (this->resolution_) {
case 9:
return 94;
case 10:
return 188;
case 11:
return 375;
default:
return 750;
}
}
void DallasComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up DallasComponent...");
pin_->setup();
// clear bus with 480µs high, otherwise initial reset in search_vec() fails
pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
delayMicroseconds(480);
one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory)
std::vector<uint64_t> raw_sensors;
raw_sensors = this->one_wire_->search_vec();
for (auto &address : raw_sensors) {
auto *address8 = reinterpret_cast<uint8_t *>(&address);
if (crc8(address8, 7) != address8[7]) {
ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str());
continue;
}
if (address8[0] != DALLAS_MODEL_DS18S20 && address8[0] != DALLAS_MODEL_DS1822 &&
address8[0] != DALLAS_MODEL_DS18B20 && address8[0] != DALLAS_MODEL_DS1825 &&
address8[0] != DALLAS_MODEL_DS28EA00) {
ESP_LOGW(TAG, "Unknown device type 0x%02X.", address8[0]);
continue;
}
this->found_sensors_.push_back(address);
}
for (auto *sensor : this->sensors_) {
if (sensor->get_index().has_value()) {
if (*sensor->get_index() >= this->found_sensors_.size()) {
this->status_set_error("Sensor configured by index but not found");
continue;
}
sensor->set_address(this->found_sensors_[*sensor->get_index()]);
}
if (!sensor->setup_sensor()) {
this->status_set_error();
}
}
}
void DallasComponent::dump_config() {
ESP_LOGCONFIG(TAG, "DallasComponent:");
LOG_PIN(" Pin: ", this->pin_);
LOG_UPDATE_INTERVAL(this);
if (this->found_sensors_.empty()) {
ESP_LOGW(TAG, " Found no sensors!");
} else {
ESP_LOGD(TAG, " Found sensors:");
for (auto &address : this->found_sensors_) {
ESP_LOGD(TAG, " 0x%s", format_hex(address).c_str());
}
}
for (auto *sensor : this->sensors_) {
LOG_SENSOR(" ", "Device", sensor);
if (sensor->get_index().has_value()) {
ESP_LOGCONFIG(TAG, " Index %u", *sensor->get_index());
if (*sensor->get_index() >= this->found_sensors_.size()) {
ESP_LOGE(TAG, "Couldn't find sensor by index - not connected. Proceeding without it.");
continue;
}
}
ESP_LOGCONFIG(TAG, " Address: %s", sensor->get_address_name().c_str());
ESP_LOGCONFIG(TAG, " Resolution: %u", sensor->get_resolution());
}
}
void DallasComponent::register_sensor(DallasTemperatureSensor *sensor) { this->sensors_.push_back(sensor); }
void DallasComponent::update() {
this->status_clear_warning();
bool result;
{
InterruptLock lock;
result = this->one_wire_->reset();
}
if (!result) {
if (!this->found_sensors_.empty()) {
// Only log error if at the start sensors were found (and thus are disconnected during uptime)
ESP_LOGE(TAG, "Requesting conversion failed");
this->status_set_warning();
}
for (auto *sensor : this->sensors_) {
sensor->publish_state(NAN);
}
return;
}
{
InterruptLock lock;
this->one_wire_->skip();
this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION);
}
for (auto *sensor : this->sensors_) {
if (sensor->get_address() == 0) {
ESP_LOGV(TAG, "'%s' - Indexed sensor not found at startup, skipping update", sensor->get_name().c_str());
sensor->publish_state(NAN);
continue;
}
this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] {
bool res = sensor->read_scratch_pad();
if (!res) {
ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str());
sensor->publish_state(NAN);
this->status_set_warning();
return;
}
if (!sensor->check_scratch_pad()) {
sensor->publish_state(NAN);
this->status_set_warning();
return;
}
float tempc = sensor->get_temp_c();
ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", sensor->get_name().c_str(), tempc);
sensor->publish_state(tempc);
});
}
}
void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; }
uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; }
void DallasTemperatureSensor::set_resolution(uint8_t resolution) { this->resolution_ = resolution; }
optional<uint8_t> DallasTemperatureSensor::get_index() const { return this->index_; }
void DallasTemperatureSensor::set_index(uint8_t index) { this->index_ = index; }
uint8_t *DallasTemperatureSensor::get_address8() { return reinterpret_cast<uint8_t *>(&this->address_); }
uint64_t DallasTemperatureSensor::get_address() { return this->address_; }
const std::string &DallasTemperatureSensor::get_address_name() {
if (this->address_name_.empty()) {
this->address_name_ = std::string("0x") + format_hex(this->address_);
}
return this->address_name_;
}
bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() {
auto *wire = this->parent_->one_wire_;
{
InterruptLock lock;
if (!wire->reset()) {
return false;
}
wire->select(this->address_);
wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD);
for (unsigned char &i : this->scratch_pad_) {
i = wire->read8();
}
}
return true;
}
bool DallasTemperatureSensor::setup_sensor() {
bool r = this->read_scratch_pad();
if (!r) {
ESP_LOGE(TAG, "Reading scratchpad failed: reset");
return false;
}
if (!this->check_scratch_pad())
return false;
if (this->scratch_pad_[4] == this->resolution_)
return false;
if (this->get_address8()[0] == DALLAS_MODEL_DS18S20) {
// DS18S20 doesn't support resolution.
ESP_LOGW(TAG, "DS18S20 doesn't support setting resolution.");
return false;
}
switch (this->resolution_) {
case 12:
this->scratch_pad_[4] = 0x7F;
break;
case 11:
this->scratch_pad_[4] = 0x5F;
break;
case 10:
this->scratch_pad_[4] = 0x3F;
break;
case 9:
default:
this->scratch_pad_[4] = 0x1F;
break;
}
auto *wire = this->parent_->one_wire_;
{
InterruptLock lock;
if (wire->reset()) {
wire->select(this->address_);
wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD);
wire->write8(this->scratch_pad_[2]); // high alarm temp
wire->write8(this->scratch_pad_[3]); // low alarm temp
wire->write8(this->scratch_pad_[4]); // resolution
wire->reset();
// write value to EEPROM
wire->select(this->address_);
wire->write8(0x48);
}
}
delay(20); // allow it to finish operation
wire->reset();
return true;
}
bool DallasTemperatureSensor::check_scratch_pad() {
bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]);
bool config_validity = false;
switch (this->get_address8()[0]) {
case DALLAS_MODEL_DS18B20:
config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F);
break;
default:
config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10);
}
#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
crc8(this->scratch_pad_, 8));
#endif
if (!chksum_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
} else if (!config_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str());
}
return chksum_validity && config_validity;
}
float DallasTemperatureSensor::get_temp_c() {
int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3);
if (this->get_address8()[0] == DALLAS_MODEL_DS18S20) {
int diff = (this->scratch_pad_[7] - this->scratch_pad_[6]) << 7;
temp = ((temp & 0xFFF0) << 3) - 16 + (diff / this->scratch_pad_[7]);
}
return temp / 128.0f;
}
std::string DallasTemperatureSensor::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); }
} // namespace dallas
} // namespace esphome

View file

@ -1,79 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esp_one_wire.h"
#include <vector>
namespace esphome {
namespace dallas {
class DallasTemperatureSensor;
class DallasComponent : public PollingComponent {
public:
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void register_sensor(DallasTemperatureSensor *sensor);
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void update() override;
protected:
friend DallasTemperatureSensor;
InternalGPIOPin *pin_;
ESPOneWire *one_wire_;
std::vector<DallasTemperatureSensor *> sensors_;
std::vector<uint64_t> found_sensors_;
};
/// Internal class that helps us create multiple sensors for one Dallas hub.
class DallasTemperatureSensor : public sensor::Sensor {
public:
void set_parent(DallasComponent *parent) { parent_ = parent; }
/// Helper to get a pointer to the address as uint8_t.
uint8_t *get_address8();
uint64_t get_address();
/// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29".
const std::string &get_address_name();
/// Set the 64-bit unsigned address for this sensor.
void set_address(uint64_t address);
/// Get the index of this sensor. (0 if using address.)
optional<uint8_t> get_index() const;
/// Set the index of this sensor. If using index, address will be set after setup.
void set_index(uint8_t index);
/// Get the set resolution for this sensor.
uint8_t get_resolution() const;
/// Set the resolution for this sensor.
void set_resolution(uint8_t resolution);
/// Get the number of milliseconds we have to wait for the conversion phase.
uint16_t millis_to_wait_for_conversion() const;
bool setup_sensor();
bool read_scratch_pad();
bool check_scratch_pad();
float get_temp_c();
std::string unique_id() override;
protected:
DallasComponent *parent_;
uint64_t address_;
optional<uint8_t> index_;
uint8_t resolution_;
std::string address_name_;
uint8_t scratch_pad_[9] = {
0,
};
};
} // namespace dallas
} // namespace esphome

View file

@ -1,252 +0,0 @@
#include "esp_one_wire.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace dallas {
static const char *const TAG = "dallas.one_wire";
const uint8_t ONE_WIRE_ROM_SELECT = 0x55;
const int ONE_WIRE_ROM_SEARCH = 0xF0;
ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); }
bool HOT IRAM_ATTR ESPOneWire::reset() {
// See reset here:
// https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
// Wait for communication to clear (delay G)
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
uint8_t retries = 125;
do {
if (--retries == 0)
return false;
delayMicroseconds(2);
} while (!pin_.digital_read());
// Send 480µs LOW TX reset pulse (drive bus low, delay H)
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
delayMicroseconds(480);
// Release the bus, delay I
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
delayMicroseconds(70);
// sample bus, 0=device(s) present, 1=no device present
bool r = !pin_.digital_read();
// delay J
delayMicroseconds(410);
return r;
}
void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) {
// drive bus low
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
// from datasheet:
// write 0 low time: t_low0: min=60µs, max=120µs
// write 1 low time: t_low1: min=1µs, max=15µs
// time slot: t_slot: min=60µs, max=120µs
// recovery time: t_rec: min=1µs
// ds18b20 appears to read the bus after roughly 14µs
uint32_t delay0 = bit ? 6 : 60;
uint32_t delay1 = bit ? 54 : 5;
// delay A/C
delayMicroseconds(delay0);
// release bus
pin_.digital_write(true);
// delay B/D
delayMicroseconds(delay1);
}
bool HOT IRAM_ATTR ESPOneWire::read_bit() {
// drive bus low
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
// note: for reading we'll need very accurate timing, as the
// timing for the digital_read() is tight; according to the datasheet,
// we should read at the end of 16µs starting from the bus low
// typically, the ds18b20 pulls the line high after 11µs for a logical 1
// and 29µs for a logical 0
uint32_t start = micros();
// datasheet says >1µs
delayMicroseconds(3);
// release bus, delay E
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
// Unfortunately some frameworks have different characteristics than others
// esp32 arduino appears to pull the bus low only after the digital_write(false),
// whereas on esp-idf it already happens during the pin_mode(OUTPUT)
// manually correct for this with these constants.
#ifdef USE_ESP32
uint32_t timing_constant = 12;
#else
uint32_t timing_constant = 14;
#endif
// measure from start value directly, to get best accurate timing no matter
// how long pin_mode/delayMicroseconds took
while (micros() - start < timing_constant)
;
// sample bus to read bit from peer
bool r = pin_.digital_read();
// read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked
uint32_t now = micros();
if (now - start < 60)
delayMicroseconds(60 - (now - start));
return r;
}
void IRAM_ATTR ESPOneWire::write8(uint8_t val) {
for (uint8_t i = 0; i < 8; i++) {
this->write_bit(bool((1u << i) & val));
}
}
void IRAM_ATTR ESPOneWire::write64(uint64_t val) {
for (uint8_t i = 0; i < 64; i++) {
this->write_bit(bool((1ULL << i) & val));
}
}
uint8_t IRAM_ATTR ESPOneWire::read8() {
uint8_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
ret |= (uint8_t(this->read_bit()) << i);
}
return ret;
}
uint64_t IRAM_ATTR ESPOneWire::read64() {
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
ret |= (uint64_t(this->read_bit()) << i);
}
return ret;
}
void IRAM_ATTR ESPOneWire::select(uint64_t address) {
this->write8(ONE_WIRE_ROM_SELECT);
this->write64(address);
}
void IRAM_ATTR ESPOneWire::reset_search() {
this->last_discrepancy_ = 0;
this->last_device_flag_ = false;
this->rom_number_ = 0;
}
uint64_t IRAM_ATTR ESPOneWire::search() {
if (this->last_device_flag_) {
return 0u;
}
{
InterruptLock lock;
if (!this->reset()) {
// Reset failed or no devices present
this->reset_search();
return 0u;
}
}
uint8_t id_bit_number = 1;
uint8_t last_zero = 0;
uint8_t rom_byte_number = 0;
bool search_result = false;
uint8_t rom_byte_mask = 1;
{
InterruptLock lock;
// Initiate search
this->write8(ONE_WIRE_ROM_SEARCH);
do {
// read bit
bool id_bit = this->read_bit();
// read its complement
bool cmp_id_bit = this->read_bit();
if (id_bit && cmp_id_bit) {
// No devices participating in search
break;
}
bool branch;
if (id_bit != cmp_id_bit) {
// only chose one branch, the other one doesn't have any devices.
branch = id_bit;
} else {
// there are devices with both 0s and 1s at this bit
if (id_bit_number < this->last_discrepancy_) {
branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0;
} else {
branch = id_bit_number == this->last_discrepancy_;
}
if (!branch) {
last_zero = id_bit_number;
}
}
if (branch) {
// set bit
this->rom_number8_()[rom_byte_number] |= rom_byte_mask;
} else {
// clear bit
this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask;
}
// choose/announce branch
this->write_bit(branch);
id_bit_number++;
rom_byte_mask <<= 1;
if (rom_byte_mask == 0u) {
// go to next byte
rom_byte_number++;
rom_byte_mask = 1;
}
} while (rom_byte_number < 8); // loop through all bytes
}
if (id_bit_number >= 65) {
this->last_discrepancy_ = last_zero;
if (this->last_discrepancy_ == 0) {
// we're at root and have no choices left, so this was the last one.
this->last_device_flag_ = true;
}
search_result = true;
}
search_result = search_result && (this->rom_number8_()[0] != 0);
if (!search_result) {
this->reset_search();
return 0u;
}
return this->rom_number_;
}
std::vector<uint64_t> ESPOneWire::search_vec() {
std::vector<uint64_t> res;
this->reset_search();
uint64_t address;
while ((address = this->search()) != 0u)
res.push_back(address);
return res;
}
void IRAM_ATTR ESPOneWire::skip() {
this->write8(0xCC); // skip ROM
}
uint8_t IRAM_ATTR *ESPOneWire::rom_number8_() { return reinterpret_cast<uint8_t *>(&this->rom_number_); }
} // namespace dallas
} // namespace esphome

View file

@ -1,68 +0,0 @@
#pragma once
#include "esphome/core/hal.h"
#include <vector>
namespace esphome {
namespace dallas {
extern const uint8_t ONE_WIRE_ROM_SELECT;
extern const int ONE_WIRE_ROM_SEARCH;
class ESPOneWire {
public:
explicit ESPOneWire(InternalGPIOPin *pin);
/** Reset the bus, should be done before all write operations.
*
* Takes approximately 1ms.
*
* @return Whether the operation was successful.
*/
bool reset();
/// Write a single bit to the bus, takes about 70µs.
void write_bit(bool bit);
/// Read a single bit from the bus, takes about 70µs
bool read_bit();
/// Write a word to the bus. LSB first.
void write8(uint8_t val);
/// Write a 64 bit unsigned integer to the bus. LSB first.
void write64(uint64_t val);
/// Write a command to the bus that addresses all devices by skipping the ROM.
void skip();
/// Read an 8 bit word from the bus.
uint8_t read8();
/// Read an 64-bit unsigned integer from the bus.
uint64_t read64();
/// Select a specific address on the bus for the following command.
void select(uint64_t address);
/// Reset the device search.
void reset_search();
/// Search for a 1-Wire device on the bus. Returns 0 if all devices have been found.
uint64_t search();
/// Helper that wraps search in a std::vector.
std::vector<uint64_t> search_vec();
protected:
/// Helper to get the internal 64-bit unsigned rom number as a 8-bit integer pointer.
inline uint8_t *rom_number8_();
ISRInternalGPIOPin pin_;
uint8_t last_discrepancy_{0};
bool last_device_flag_{false};
uint64_t rom_number_{0};
};
} // namespace dallas
} // namespace esphome

View file

@ -1,50 +1,5 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ADDRESS,
CONF_DALLAS_ID,
CONF_INDEX,
CONF_RESOLUTION,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
CONFIG_SCHEMA = cv.invalid(
'The "dallas" sensor is now "dallas_temp"\nhttps://esphome.io/components/sensor/dallas_temp'
)
from . import DallasComponent, dallas_ns
DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sensor)
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
DallasTemperatureSensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent),
cv.Optional(CONF_ADDRESS): cv.hex_uint64_t,
cv.Optional(CONF_INDEX): cv.positive_int,
cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12),
}
),
cv.has_exactly_one_key(CONF_ADDRESS, CONF_INDEX),
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DALLAS_ID])
var = await sensor.new_sensor(config)
if CONF_ADDRESS in config:
cg.add(var.set_address(config[CONF_ADDRESS]))
else:
cg.add(var.set_index(config[CONF_INDEX]))
if CONF_RESOLUTION in config:
cg.add(var.set_resolution(config[CONF_RESOLUTION]))
cg.add(var.set_parent(hub))
cg.add(hub.register_sensor(var))

View file

@ -0,0 +1 @@
CODEOWNERS = ["@ssieb"]

View file

@ -0,0 +1,172 @@
#include "dallas_temp.h"
#include "esphome/core/log.h"
namespace esphome {
namespace dallas_temp {
static const char *const TAG = "dallas.temp.sensor";
static const uint8_t DALLAS_MODEL_DS18S20 = 0x10;
static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44;
static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE;
static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E;
static const uint8_t DALLAS_COMMAND_COPY_SCRATCH_PAD = 0x48;
uint16_t DallasTemperatureSensor::millis_to_wait_for_conversion_() const {
switch (this->resolution_) {
case 9:
return 94;
case 10:
return 188;
case 11:
return 375;
default:
return 750;
}
}
void DallasTemperatureSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Dallas Temperature Sensor:");
if (this->address_ == 0) {
ESP_LOGW(TAG, " Unable to select an address");
return;
}
LOG_ONE_WIRE_DEVICE(this);
ESP_LOGCONFIG(TAG, " Resolution: %u bits", this->resolution_);
LOG_UPDATE_INTERVAL(this);
}
void DallasTemperatureSensor::update() {
if (this->address_ == 0)
return;
this->status_clear_warning();
this->send_command_(DALLAS_COMMAND_START_CONVERSION);
this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] {
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
this->publish_state(NAN);
return;
}
float tempc = this->get_temp_c_();
ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc);
this->publish_state(tempc);
});
}
void IRAM_ATTR DallasTemperatureSensor::read_scratch_pad_int_() {
for (uint8_t &i : this->scratch_pad_) {
i = this->bus_->read8();
}
}
bool DallasTemperatureSensor::read_scratch_pad_() {
bool success;
{
InterruptLock lock;
success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD);
if (success)
this->read_scratch_pad_int_();
}
if (!success) {
ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str());
this->status_set_warning("bus reset failed");
}
return success;
}
void DallasTemperatureSensor::setup() {
ESP_LOGCONFIG(TAG, "setting up Dallas temperature sensor...");
if (!this->check_address_())
return;
if (!this->read_scratch_pad_())
return;
if (!this->check_scratch_pad_())
return;
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
// DS18S20 doesn't support resolution.
ESP_LOGW(TAG, "DS18S20 doesn't support setting resolution.");
return;
}
uint8_t res;
switch (this->resolution_) {
case 12:
res = 0x7F;
break;
case 11:
res = 0x5F;
break;
case 10:
res = 0x3F;
break;
case 9:
default:
res = 0x1F;
break;
}
if (this->scratch_pad_[4] == res)
return;
this->scratch_pad_[4] = res;
{
InterruptLock lock;
if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) {
this->bus_->write8(this->scratch_pad_[2]); // high alarm temp
this->bus_->write8(this->scratch_pad_[3]); // low alarm temp
this->bus_->write8(this->scratch_pad_[4]); // resolution
}
// write value to EEPROM
this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD);
}
}
bool DallasTemperatureSensor::check_scratch_pad_() {
bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]);
#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
crc8(this->scratch_pad_, 8));
#endif
if (!chksum_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
this->status_set_warning("scratch pad checksum invalid");
}
return chksum_validity;
}
float DallasTemperatureSensor::get_temp_c_() {
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
if (this->scratch_pad_[7] != 0x10)
ESP_LOGE(TAG, "unexpected COUNT_PER_C value: %u", this->scratch_pad_[7]);
temp = ((temp & 0xfff7) << 3) + (0x10 - this->scratch_pad_[6]) - 4;
} else {
switch (this->resolution_) {
case 9:
temp &= 0xfff8;
break;
case 10:
temp &= 0xfffc;
break;
case 11:
temp &= 0xfffe;
break;
case 12:
default:
break;
}
}
return temp / 16.0f;
}
} // namespace dallas_temp
} // namespace esphome

View file

@ -0,0 +1,32 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/one_wire/one_wire.h"
namespace esphome {
namespace dallas_temp {
class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor, public one_wire::OneWireDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
/// Set the resolution for this sensor.
void set_resolution(uint8_t resolution) { this->resolution_ = resolution; }
protected:
uint8_t resolution_;
uint8_t scratch_pad_[9] = {0};
/// Get the number of milliseconds we have to wait for the conversion phase.
uint16_t millis_to_wait_for_conversion_() const;
bool read_scratch_pad_();
void read_scratch_pad_int_();
bool check_scratch_pad_();
float get_temp_c_();
};
} // namespace dallas_temp
} // namespace esphome

View file

@ -0,0 +1,43 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import one_wire, sensor
from esphome.const import (
CONF_RESOLUTION,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
dallas_temp_ns = cg.esphome_ns.namespace("dallas_temp")
DallasTemperatureSensor = dallas_temp_ns.class_(
"DallasTemperatureSensor",
cg.PollingComponent,
sensor.Sensor,
one_wire.OneWireDevice,
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
DallasTemperatureSensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12),
}
)
.extend(one_wire.one_wire_device_schema())
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await one_wire.register_one_wire_device(var, config)
cg.add(var.set_resolution(config[CONF_RESOLUTION]))

View file

@ -80,6 +80,17 @@ void DateCall::validate_() {
void DateCall::perform() {
this->validate_();
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (this->year_.has_value()) {
ESP_LOGD(TAG, " Year: %d", *this->year_);
}
if (this->month_.has_value()) {
ESP_LOGD(TAG, " Month: %d", *this->month_);
}
if (this->day_.has_value()) {
ESP_LOGD(TAG, " Day: %d", *this->day_);
}
this->parent_->control(*this);
}

View file

@ -34,10 +34,12 @@ enum WakeupPinMode {
WAKEUP_PIN_MODE_INVERT_WAKEUP,
};
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
struct Ext1Wakeup {
uint64_t mask;
esp_sleep_ext1_wakeup_mode_t wakeup_mode;
};
#endif
struct WakeupCauseToRunDuration {
// Run duration if woken up by timer or any other reason besides those below.
@ -114,7 +116,11 @@ class DeepSleepComponent : public Component {
#ifdef USE_ESP32
InternalGPIOPin *wakeup_pin_;
WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
#if !defined(USE_ESP32_VARIANT_ESP32C3)
optional<Ext1Wakeup> ext1_wakeup_;
#endif
optional<bool> touch_wakeup_;
optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_;
#endif

View file

@ -96,16 +96,16 @@ def get_board(core_obj=None):
def get_download_types(storage_json):
return [
{
"title": "Modern format",
"title": "Factory format (Previously Modern)",
"description": "For use with ESPHome Web and other tools.",
"file": "firmware-factory.bin",
"download": f"{storage_json.name}-factory.bin",
"file": "firmware.factory.bin",
"download": f"{storage_json.name}.factory.bin",
},
{
"title": "Legacy format",
"description": "For use with ESPHome Flasher.",
"file": "firmware.bin",
"download": f"{storage_json.name}.bin",
"title": "OTA format (Previously Legacy)",
"description": "For OTA updating a device.",
"file": "firmware.ota.bin",
"download": f"{storage_json.name}.ota.bin",
},
]

View file

@ -17,17 +17,19 @@ from SCons.Script import ARGUMENTS
# Copy over the default sdkconfig.
from os import path
if path.exists("./sdkconfig.defaults"):
os.makedirs(".temp", exist_ok=True)
shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf")
def esp32_create_combined_bin(source, target, env):
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0")))
if verbose:
print("Generating combined binary for serial flashing")
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
@ -62,5 +64,14 @@ def esp32_create_combined_bin(source, target, env):
else:
subprocess.run(["esptool.py", *cmd])
def esp32_copy_ota_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa

View file

@ -17,7 +17,7 @@ from esphome.const import (
CONF_VSYNC_PIN,
)
from esphome.core import CORE
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32 import add_idf_component
from esphome.cpp_helpers import setup_entity
DEPENDENCIES = ["esp32"]
@ -290,8 +290,11 @@ async def to_code(config):
cg.add_define("USE_ESP32_CAMERA")
if CORE.using_esp_idf:
cg.add_library("espressif/esp32-camera", "1.0.0")
add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True)
add_idf_component(
name="esp32-camera",
repo="https://github.com/espressif/esp32-camera.git",
ref="v2.0.9",
)
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View file

@ -6,10 +6,18 @@ Import("env") # noqa
def esp8266_copy_factory_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
shutil.copyfile(firmware_name, new_file_name)
def esp8266_copy_ota_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_factory_bin) # noqa
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_ota_bin) # noqa

View file

@ -631,7 +631,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page 0x%02" PRIX32, 0x0);
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
}

View file

@ -0,0 +1,25 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_PIN
from esphome.components.one_wire import OneWireBus
from .. import gpio_ns
CODEOWNERS = ["@ssieb"]
GPIOOneWireBus = gpio_ns.class_("GPIOOneWireBus", OneWireBus, cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(GPIOOneWireBus),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))

View file

@ -0,0 +1,199 @@
#include "gpio_one_wire.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace gpio {
static const char *const TAG = "gpio.one_wire";
void GPIOOneWireBus::setup() {
ESP_LOGCONFIG(TAG, "Setting up 1-wire bus...");
this->search();
}
void GPIOOneWireBus::dump_config() {
ESP_LOGCONFIG(TAG, "GPIO 1-wire bus:");
LOG_PIN(" Pin: ", this->t_pin_);
this->dump_devices_(TAG);
}
bool HOT IRAM_ATTR GPIOOneWireBus::reset() {
// See reset here:
// https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
// Wait for communication to clear (delay G)
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
uint8_t retries = 125;
do {
if (--retries == 0)
return false;
delayMicroseconds(2);
} while (!pin_.digital_read());
bool r;
// Send 480µs LOW TX reset pulse (drive bus low, delay H)
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
delayMicroseconds(480);
// Release the bus, delay I
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
delayMicroseconds(70);
// sample bus, 0=device(s) present, 1=no device present
r = !pin_.digital_read();
// delay J
delayMicroseconds(410);
return r;
}
void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) {
// drive bus low
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
// from datasheet:
// write 0 low time: t_low0: min=60µs, max=120µs
// write 1 low time: t_low1: min=1µs, max=15µs
// time slot: t_slot: min=60µs, max=120µs
// recovery time: t_rec: min=1µs
// ds18b20 appears to read the bus after roughly 14µs
uint32_t delay0 = bit ? 6 : 60;
uint32_t delay1 = bit ? 59 : 5;
// delay A/C
delayMicroseconds(delay0);
// release bus
pin_.digital_write(true);
// delay B/D
delayMicroseconds(delay1);
}
bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() {
// drive bus low
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
// note: for reading we'll need very accurate timing, as the
// timing for the digital_read() is tight; according to the datasheet,
// we should read at the end of 16µs starting from the bus low
// typically, the ds18b20 pulls the line high after 11µs for a logical 1
// and 29µs for a logical 0
uint32_t start = micros();
// datasheet says >1µs
delayMicroseconds(2);
// release bus, delay E
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
// measure from start value directly, to get best accurate timing no matter
// how long pin_mode/delayMicroseconds took
delayMicroseconds(12 - (micros() - start));
// sample bus to read bit from peer
bool r = pin_.digital_read();
// read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked
uint32_t now = micros();
if (now - start < 60)
delayMicroseconds(60 - (now - start));
return r;
}
void IRAM_ATTR GPIOOneWireBus::write8(uint8_t val) {
for (uint8_t i = 0; i < 8; i++) {
this->write_bit_(bool((1u << i) & val));
}
}
void IRAM_ATTR GPIOOneWireBus::write64(uint64_t val) {
for (uint8_t i = 0; i < 64; i++) {
this->write_bit_(bool((1ULL << i) & val));
}
}
uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
uint8_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
ret |= (uint8_t(this->read_bit_()) << i);
}
return ret;
}
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
ret |= (uint64_t(this->read_bit_()) << i);
}
return ret;
}
void GPIOOneWireBus::reset_search() {
this->last_discrepancy_ = 0;
this->last_device_flag_ = false;
this->address_ = 0;
}
uint64_t IRAM_ATTR GPIOOneWireBus::search_int() {
if (this->last_device_flag_)
return 0u;
uint8_t last_zero = 0;
uint64_t bit_mask = 1;
uint64_t address = this->address_;
// Initiate search
for (int bit_number = 1; bit_number <= 64; bit_number++, bit_mask <<= 1) {
// read bit
bool id_bit = this->read_bit_();
// read its complement
bool cmp_id_bit = this->read_bit_();
if (id_bit && cmp_id_bit) {
// No devices participating in search
return 0;
}
bool branch;
if (id_bit != cmp_id_bit) {
// only chose one branch, the other one doesn't have any devices.
branch = id_bit;
} else {
// there are devices with both 0s and 1s at this bit
if (bit_number < this->last_discrepancy_) {
branch = (address & bit_mask) > 0;
} else {
branch = bit_number == this->last_discrepancy_;
}
if (!branch) {
last_zero = bit_number;
}
}
if (branch) {
address |= bit_mask;
} else {
address &= ~bit_mask;
}
// choose/announce branch
this->write_bit_(branch);
}
this->last_discrepancy_ = last_zero;
if (this->last_discrepancy_ == 0) {
// we're at root and have no choices left, so this was the last one.
this->last_device_flag_ = true;
}
this->address_ = address;
return address;
}
} // namespace gpio
} // namespace esphome

View file

@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/one_wire/one_wire.h"
namespace esphome {
namespace gpio {
class GPIOOneWireBus : public one_wire::OneWireBus, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_pin(InternalGPIOPin *pin) {
this->t_pin_ = pin;
this->pin_ = pin->to_isr();
}
bool reset() override;
void write8(uint8_t val) override;
void write64(uint64_t val) override;
uint8_t read8() override;
uint64_t read64() override;
protected:
InternalGPIOPin *t_pin_;
ISRInternalGPIOPin pin_;
uint8_t last_discrepancy_{0};
bool last_device_flag_{false};
uint64_t address_;
void reset_search() override;
uint64_t search_int() override;
void write_bit_(bool bit);
bool read_bit_();
};
} // namespace gpio
} // namespace esphome

View file

@ -56,7 +56,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) {
this->position = new_position;
this->current_operation = COVER_OPERATION_IDLE;
if (this->last_command_ == operation) {
float dur = (now - this->start_dir_time_) / 1e3f;
float dur = (float) (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
}
@ -69,7 +69,6 @@ void HE60rCover::set_current_operation_(cover::CoverOperation operation) {
this->current_operation = operation;
if (operation != COVER_OPERATION_IDLE)
this->last_recompute_time_ = millis();
this->publish_state();
}
}
@ -129,7 +128,7 @@ void HE60rCover::update_() {
if (this->toggles_needed_ != 0) {
if ((this->counter_++ & 0x3) == 0) {
this->toggles_needed_--;
ESP_LOGD(TAG, "Writing byte 0x30, still needed=%" PRIu32, this->toggles_needed_);
ESP_LOGD(TAG, "Writing byte 0x30, still needed=%u", this->toggles_needed_);
this->write_byte(TOGGLE_BYTE);
} else {
this->write_byte(QUERY_BYTE);
@ -235,33 +234,30 @@ void HE60rCover::recompute_position_() {
return;
const uint32_t now = millis();
float dir;
float action_dur;
if (now > this->last_recompute_time_) {
auto diff = (unsigned) (now - last_recompute_time_);
float delta;
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
dir = 1.0f;
action_dur = this->open_duration_;
delta = (float) diff / (float) this->open_duration_;
break;
case COVER_OPERATION_CLOSING:
dir = -1.0f;
action_dur = this->close_duration_;
delta = -(float) diff / (float) this->close_duration_;
break;
default:
return;
}
if (now > this->last_recompute_time_) {
auto diff = now - last_recompute_time_;
auto delta = dir * diff / action_dur;
// make sure our guesstimate never reaches full open or close.
this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f);
ESP_LOGD(TAG, "Recompute %dms, dir=%f, action_dur=%f, delta=%f, pos=%f", (int) diff, dir, action_dur, delta,
this->position);
auto new_position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f);
ESP_LOGD(TAG, "Recompute %ums, dir=%u, delta=%f, pos=%f", diff, this->current_operation, delta, new_position);
this->last_recompute_time_ = now;
if (this->position != new_position) {
this->position = new_position;
this->publish_state();
}
}
}
} // namespace he60r
} // namespace esphome

View file

@ -25,15 +25,14 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic
void control(const cover::CoverCall &call) override;
bool is_at_target_() const;
void start_direction_(cover::CoverOperation dir);
void update_operation_(cover::CoverOperation dir);
void endstop_reached_(cover::CoverOperation operation);
void recompute_position_();
void set_current_operation_(cover::CoverOperation operation);
void process_rx_(uint8_t data);
uint32_t open_duration_{0};
uint32_t close_duration_{0};
uint32_t toggles_needed_{0};
unsigned open_duration_{0};
unsigned close_duration_{0};
unsigned toggles_needed_{0};
cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE};
cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE};
uint32_t last_recompute_time_{0};

View file

@ -1,9 +1,8 @@
import urllib.parse as urlparse
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.const import (
__version__,
CONF_ID,
CONF_TIMEOUT,
CONF_METHOD,
@ -12,67 +11,91 @@ from esphome.const import (
CONF_ESP8266_DISABLE_SSL_SUPPORT,
)
from esphome.core import Lambda, CORE
from esphome.components import esp32
DEPENDENCIES = ["network"]
AUTO_LOAD = ["json"]
http_request_ns = cg.esphome_ns.namespace("http_request")
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent)
HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
HttpContainer = http_request_ns.class_("HttpContainer")
HttpRequestSendAction = http_request_ns.class_(
"HttpRequestSendAction", automation.Action
)
HttpRequestResponseTrigger = http_request_ns.class_(
"HttpRequestResponseTrigger", automation.Trigger
"HttpRequestResponseTrigger",
automation.Trigger.template(
cg.std_shared_ptr.template(HttpContainer), cg.std_string
),
)
CONF_HEADERS = "headers"
CONF_HTTP_REQUEST_ID = "http_request_id"
CONF_USERAGENT = "useragent"
CONF_BODY = "body"
CONF_JSON = "json"
CONF_VERIFY_SSL = "verify_ssl"
CONF_ON_RESPONSE = "on_response"
CONF_FOLLOW_REDIRECTS = "follow_redirects"
CONF_REDIRECT_LIMIT = "redirect_limit"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
CONF_ON_RESPONSE = "on_response"
CONF_HEADERS = "headers"
CONF_BODY = "body"
CONF_JSON = "json"
CONF_CAPTURE_RESPONSE = "capture_response"
def validate_url(value):
value = cv.string(value)
try:
parsed = list(urlparse.urlparse(value))
except Exception as err:
raise cv.Invalid("Invalid URL") from err
if not parsed[0] or not parsed[1]:
raise cv.Invalid("URL must have a URL scheme and host")
if parsed[0] not in ["http", "https"]:
raise cv.Invalid("Scheme must be http or https")
if not parsed[2]:
parsed[2] = "/"
return urlparse.urlunparse(parsed)
value = cv.url(value)
if value.startswith("http://") or value.startswith("https://"):
return value
raise cv.Invalid("URL must start with 'http://' or 'https://'")
def validate_secure_url(config):
url_ = config[CONF_URL]
def validate_ssl_verification(config):
error_message = ""
if CORE.is_esp32:
if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
error_message = "ESPHome supports certificate verification only via ESP-IDF"
if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
error_message = "ESPHome does not support certificate verification on RP2040"
if (
config.get(CONF_VERIFY_SSL)
and not isinstance(url_, Lambda)
and url_.lower().startswith("https:")
CORE.is_esp8266
and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
and config[CONF_VERIFY_SSL]
):
error_message = "ESPHome does not support certificate verification on ESP8266"
if len(error_message) > 0:
raise cv.Invalid(
"Currently ESPHome doesn't support SSL verification. "
"Set 'verify_ssl: false' to make insecure HTTPS requests."
f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
)
return config
def _declare_request_class(value):
if CORE.using_esp_idf:
return cv.declare_id(HttpRequestIDF)(value)
if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
return cv.declare_id(HttpRequestArduino)(value)
return NotImplementedError
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HttpRequestComponent),
cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
cv.GenerateID(): _declare_request_class,
cv.Optional(
CONF_USERAGENT, f"ESPHome/{__version__} (https://esphome.io)"
): cv.string,
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
cv.Optional(
@ -81,12 +104,21 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
cv.only_on_esp8266, cv.boolean
),
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All(
cv.Any(cv.only_on_esp32, cv.only_on_rp2040),
cv.positive_not_null_time_period,
cv.positive_time_period_milliseconds,
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 5, 1),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(0, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0),
),
validate_ssl_verification,
)
@ -100,11 +132,30 @@ async def to_code(config):
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT):
cg.add(var.set_watchdog_timeout(timeout_ms))
if CORE.is_esp32:
if CORE.using_esp_idf:
esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
config.get(CONF_VERIFY_SSL),
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_INSECURE",
not config.get(CONF_VERIFY_SSL),
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
not config.get(CONF_VERIFY_SSL),
)
else:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("HTTPClient", None)
await cg.register_component(var, config)
@ -116,12 +167,16 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
cv.Optional(CONF_VERIFY_SSL): cv.invalid(
f"{CONF_VERIFY_SSL} has moved to the base component configuration."
),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
),
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
}
).add_extra(validate_secure_url)
)
HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf(
CONF_URL,
HTTP_REQUEST_ACTION_SCHEMA.extend(
@ -173,6 +228,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
cg.add(var.set_method(config[CONF_METHOD]))
cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE]))
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
if CONF_BODY in config:
template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string)
cg.add(var.set_body(template_))
@ -196,7 +254,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
cg.add(var.register_response_trigger(trigger))
await automation.build_automation(
trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf
trigger,
[
(cg.std_shared_ptr.template(HttpContainer), "response"),
(cg.std_string, "body"),
],
conf,
)
return var

View file

@ -1,9 +1,8 @@
#ifdef USE_ARDUINO
#include "http_request.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/components/network/util.h"
#include <cinttypes>
namespace esphome {
namespace http_request {
@ -14,131 +13,12 @@ void HttpRequestComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HTTP Request:");
ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_);
ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_);
ESP_LOGCONFIG(TAG, " Follow Redirects: %d", this->follow_redirects_);
ESP_LOGCONFIG(TAG, " Follow redirects: %s", YESNO(this->follow_redirects_));
ESP_LOGCONFIG(TAG, " Redirect limit: %d", this->redirect_limit_);
if (this->watchdog_timeout_ > 0) {
ESP_LOGCONFIG(TAG, " Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_);
}
void HttpRequestComponent::set_url(std::string url) {
this->url_ = std::move(url);
this->secure_ = this->url_.compare(0, 6, "https:") == 0;
if (!this->last_url_.empty() && this->url_ != this->last_url_) {
// Close connection if url has been changed
this->client_.setReuse(false);
this->client_.end();
}
this->client_.setReuse(true);
}
void HttpRequestComponent::send(const std::vector<HttpRequestResponseTrigger *> &response_triggers) {
if (!network::is_connected()) {
this->client_.end();
this->status_set_warning();
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
return;
}
bool begin_status = false;
const String url = this->url_.c_str();
#if defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0))
#if defined(USE_ESP32) || USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
if (this->follow_redirects_) {
this->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
} else {
this->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
}
#else
this->client_.setFollowRedirects(this->follow_redirects_);
#endif
this->client_.setRedirectLimit(this->redirect_limit_);
#endif
#if defined(USE_ESP32)
begin_status = this->client_.begin(url);
#elif defined(USE_ESP8266)
begin_status = this->client_.begin(*this->get_wifi_client_(), url);
#endif
if (!begin_status) {
this->client_.end();
this->status_set_warning();
ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration");
return;
}
this->client_.setTimeout(this->timeout_);
#if defined(USE_ESP32)
this->client_.setConnectTimeout(this->timeout_);
#endif
if (this->useragent_ != nullptr) {
this->client_.setUserAgent(this->useragent_);
}
for (const auto &header : this->headers_) {
this->client_.addHeader(header.name, header.value, false, true);
}
uint32_t start_time = millis();
int http_code = this->client_.sendRequest(this->method_, this->body_.c_str());
uint32_t duration = millis() - start_time;
for (auto *trigger : response_triggers)
trigger->process(http_code, duration);
if (http_code < 0) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(),
HTTPClient::errorToString(http_code).c_str(), duration);
this->status_set_warning();
return;
}
if (http_code < 200 || http_code >= 300) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
this->status_set_warning();
return;
}
this->status_clear_warning();
ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
}
#ifdef USE_ESP8266
std::shared_ptr<WiFiClient> HttpRequestComponent::get_wifi_client_() {
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
if (this->secure_) {
if (this->wifi_client_secure_ == nullptr) {
this->wifi_client_secure_ = std::make_shared<BearSSL::WiFiClientSecure>();
this->wifi_client_secure_->setInsecure();
this->wifi_client_secure_->setBufferSizes(512, 512);
}
return this->wifi_client_secure_;
}
#endif
if (this->wifi_client_ == nullptr) {
this->wifi_client_ = std::make_shared<WiFiClient>();
}
return this->wifi_client_;
}
#endif
void HttpRequestComponent::close() {
this->last_url_ = this->url_;
this->client_.end();
}
const char *HttpRequestComponent::get_string() {
#if defined(ESP32)
// The static variable is here because HTTPClient::getString() returns a String on ESP32,
// and we need something to keep a buffer alive.
static String str;
#else
// However on ESP8266, HTTPClient::getString() returns a String& to a member variable.
// Leaving this the default so that any new platform either doesn't copy, or encounters a compilation error.
auto &
#endif
str = this->client_.getString();
return str.c_str();
}
} // namespace http_request
} // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,27 +1,18 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/components/json/json_util.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include <list>
#include <map>
#include <memory>
#include <utility>
#include <vector>
#ifdef USE_ESP32
#include <HTTPClient.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
#include <WiFiClientSecure.h>
#endif
#endif
#include "esphome/components/json/json_util.h"
#include "esphome/core/application.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace http_request {
@ -31,9 +22,32 @@ struct Header {
const char *value;
};
class HttpRequestResponseTrigger : public Trigger<int32_t, uint32_t> {
class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
public:
void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); }
virtual ~HttpContainer() = default;
size_t content_length;
int status_code;
uint32_t duration_ms;
virtual int read(uint8_t *buf, size_t max_len) = 0;
virtual void end() = 0;
void set_secure(bool secure) { this->secure_ = secure; }
size_t get_bytes_read() const { return this->bytes_read_; }
protected:
size_t bytes_read_{0};
bool secure_{false};
};
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string> {
public:
void process(std::shared_ptr<HttpContainer> container, std::string response_body) {
this->trigger(std::move(container), std::move(response_body));
}
};
class HttpRequestComponent : public Component {
@ -41,37 +55,33 @@ class HttpRequestComponent : public Component {
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_url(std::string url);
void set_method(const char *method) { this->method_ = method; }
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
void set_body(const std::string &body) { this->body_ = body; }
void set_headers(std::list<Header> headers) { this->headers_ = std::move(headers); }
void send(const std::vector<HttpRequestResponseTrigger *> &response_triggers);
void close();
const char *get_string();
std::shared_ptr<HttpContainer> get(std::string url) { return this->start(std::move(url), "GET", "", {}); }
std::shared_ptr<HttpContainer> get(std::string url, std::list<Header> headers) {
return this->start(std::move(url), "GET", "", std::move(headers));
}
std::shared_ptr<HttpContainer> post(std::string url, std::string body) {
return this->start(std::move(url), "POST", std::move(body), {});
}
std::shared_ptr<HttpContainer> post(std::string url, std::string body, std::list<Header> headers) {
return this->start(std::move(url), "POST", std::move(body), std::move(headers));
}
virtual std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
std::list<Header> headers) = 0;
protected:
HTTPClient client_{};
std::string url_;
std::string last_url_;
const char *method_;
const char *useragent_{nullptr};
bool secure_;
bool follow_redirects_;
uint16_t redirect_limit_;
uint16_t timeout_{5000};
std::string body_;
std::list<Header> headers_;
#ifdef USE_ESP8266
std::shared_ptr<WiFiClient> wifi_client_;
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
std::shared_ptr<BearSSL::WiFiClientSecure> wifi_client_secure_;
#endif
std::shared_ptr<WiFiClient> get_wifi_client_();
#endif
uint32_t watchdog_timeout_{0};
};
template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
@ -80,6 +90,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(const char *, method)
TEMPLATABLE_VALUE(std::string, body)
TEMPLATABLE_VALUE(bool, capture_response)
void add_header(const char *key, TemplatableValue<const char *, Ts...> value) { this->headers_.insert({key, value}); }
@ -89,19 +100,22 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); }
void set_max_response_buffer_size(size_t max_response_buffer_size) {
this->max_response_buffer_size_ = max_response_buffer_size;
}
void play(Ts... x) override {
this->parent_->set_url(this->url_.value(x...));
this->parent_->set_method(this->method_.value(x...));
std::string body;
if (this->body_.has_value()) {
this->parent_->set_body(this->body_.value(x...));
body = this->body_.value(x...);
}
if (!this->json_.empty()) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
this->parent_->set_body(json::build_json(f));
body = json::build_json(f);
}
if (this->json_func_ != nullptr) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
this->parent_->set_body(json::build_json(f));
body = json::build_json(f);
}
std::list<Header> headers;
for (const auto &item : this->headers_) {
@ -111,10 +125,37 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
header.value = val.value(x...);
headers.push_back(header);
}
this->parent_->set_headers(headers);
this->parent_->send(this->response_triggers_);
this->parent_->close();
this->parent_->set_body("");
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers);
if (container == nullptr) {
return;
}
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
std::string response_body;
if (this->capture_response_.value(x...)) {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
size_t read_index = 0;
while (container->get_bytes_read() < max_length) {
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
read_index += read;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);
}
}
for (auto *trigger : this->response_triggers_) {
trigger->process(container, response_body);
}
container->end();
}
protected:
@ -130,9 +171,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
std::vector<HttpRequestResponseTrigger *> response_triggers_;
size_t max_response_buffer_size_{SIZE_MAX};
};
} // namespace http_request
} // namespace esphome
#endif // USE_ARDUINO

View file

@ -0,0 +1,161 @@
#include "http_request_arduino.h"
#ifdef USE_ARDUINO
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "watchdog.h"
namespace esphome {
namespace http_request {
static const char *const TAG = "http_request.arduino";
std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::string method, std::string body,
std::list<Header> headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
return nullptr;
}
std::shared_ptr<HttpContainerArduino> container = std::make_shared<HttpContainerArduino>();
container->set_parent(this);
const uint32_t start = millis();
bool secure = url.find("https:") != std::string::npos;
container->set_secure(secure);
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
#if defined(USE_ESP8266)
std::unique_ptr<WiFiClient> stream_ptr;
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
if (secure) {
ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
stream_ptr = std::make_unique<WiFiClientSecure>();
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
secure_client->setBufferSizes(512, 512);
secure_client->setInsecure();
} else {
stream_ptr = std::make_unique<WiFiClient>();
}
#else
ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient");
if (secure) {
ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support");
return nullptr;
}
stream_ptr = std::make_unique<WiFiClient>();
#endif // USE_HTTP_REQUEST_ESP8266_HTTPS
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?)
if (!secure) {
ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 "
"in your YAML, or use HTTPS");
}
#endif // USE_ARDUINO_VERSION_CODE
container->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
bool status = container->client_.begin(*stream_ptr, url.c_str());
#elif defined(USE_RP2040)
if (secure) {
container->client_.setInsecure();
}
bool status = container->client_.begin(url.c_str());
#elif defined(USE_ESP32)
bool status = container->client_.begin(url.c_str());
#endif
App.feed_wdt();
if (!status) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str());
container->end();
this->status_momentary_error("failed", 1000);
return nullptr;
}
container->client_.setReuse(true);
container->client_.setTimeout(this->timeout_);
#if defined(USE_ESP32)
container->client_.setConnectTimeout(this->timeout_);
#endif
if (this->useragent_ != nullptr) {
container->client_.setUserAgent(this->useragent_);
}
for (const auto &header : headers) {
container->client_.addHeader(header.name, header.value, false, true);
}
// returned needed headers must be collected before the requests
static const char *header_keys[] = {"Content-Length", "Content-Type"};
static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]);
container->client_.collectHeaders(header_keys, HEADER_COUNT);
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
if (container->status_code < 0) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
HTTPClient::errorToString(container->status_code).c_str());
this->status_momentary_error("failed", 1000);
container->end();
return nullptr;
}
if (container->status_code < 200 || container->status_code >= 300) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
this->status_momentary_error("failed", 1000);
container->end();
return nullptr;
}
int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length;
container->duration_ms = millis() - start;
return container;
}
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return -1;
}
int available_data = stream_ptr->available();
int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data));
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
}
App.feed_wdt();
int read_len = stream_ptr->readBytes(buf, bufsize);
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
return read_len;
}
void HttpContainerArduino::end() {
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->client_.end();
}
} // namespace http_request
} // namespace esphome
#endif // USE_ARDUINO

View file

@ -0,0 +1,40 @@
#pragma once
#include "http_request.h"
#ifdef USE_ARDUINO
#if defined(USE_ESP32) || defined(USE_RP2040)
#include <HTTPClient.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
#include <WiFiClientSecure.h>
#endif
#endif
namespace esphome {
namespace http_request {
class HttpRequestArduino;
class HttpContainerArduino : public HttpContainer {
public:
int read(uint8_t *buf, size_t max_len) override;
void end() override;
protected:
friend class HttpRequestArduino;
HTTPClient client_{};
};
class HttpRequestArduino : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
std::list<Header> headers) override;
};
} // namespace http_request
} // namespace esphome
#endif // USE_ARDUINO

View file

@ -0,0 +1,155 @@
#include "http_request_idf.h"
#ifdef USE_ESP_IDF
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
#endif
#include "watchdog.h"
namespace esphome {
namespace http_request {
static const char *const TAG = "http_request.idf";
std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::string method, std::string body,
std::list<Header> headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
return nullptr;
}
esp_http_client_method_t method_idf;
if (method == "GET") {
method_idf = HTTP_METHOD_GET;
} else if (method == "POST") {
method_idf = HTTP_METHOD_POST;
} else if (method == "PUT") {
method_idf = HTTP_METHOD_PUT;
} else if (method == "DELETE") {
method_idf = HTTP_METHOD_DELETE;
} else if (method == "PATCH") {
method_idf = HTTP_METHOD_PATCH;
} else {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
return nullptr;
}
bool secure = url.find("https:") != std::string::npos;
esp_http_client_config_t config = {};
config.url = url.c_str();
config.method = method_idf;
config.timeout_ms = this->timeout_;
config.disable_auto_redirect = !this->follow_redirects_;
config.max_redirection_count = this->redirect_limit_;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
#endif
if (this->useragent_ != nullptr) {
config.user_agent = this->useragent_;
}
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
esp_http_client_handle_t client = esp_http_client_init(&config);
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
container->set_parent(this);
container->set_secure(secure);
for (const auto &header : headers) {
esp_http_client_set_header(client, header.name, header.value);
}
int body_len = body.length();
esp_err_t err = esp_http_client_open(client, body_len);
if (err != ESP_OK) {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return nullptr;
}
if (body_len > 0) {
int write_left = body_len;
int write_index = 0;
const char *buf = body.c_str();
while (body_len > 0) {
int written = esp_http_client_write(client, buf + write_index, write_left);
if (written < 0) {
err = ESP_FAIL;
break;
}
write_left -= written;
write_index += written;
}
}
if (err != ESP_OK) {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return nullptr;
}
container->content_length = esp_http_client_fetch_headers(client);
const auto status_code = esp_http_client_get_status_code(client);
container->status_code = status_code;
if (status_code < 200 || status_code >= 300) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), status_code);
this->status_momentary_error("failed", 1000);
esp_http_client_cleanup(client);
return nullptr;
}
container->duration_ms = millis() - start;
return container;
}
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
}
App.feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
return read_len;
}
void HttpContainerIDF::end() {
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
}
} // namespace http_request
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -0,0 +1,34 @@
#pragma once
#include "http_request.h"
#ifdef USE_ESP_IDF
#include <esp_event.h>
#include <esp_http_client.h>
#include <esp_netif.h>
#include <esp_tls.h>
namespace esphome {
namespace http_request {
class HttpContainerIDF : public HttpContainer {
public:
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
int read(uint8_t *buf, size_t max_len) override;
void end() override;
protected:
esp_http_client_handle_t client_;
};
class HttpRequestIDF : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
std::list<Header> headers) override;
};
} // namespace http_request
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -2,92 +2,35 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.const import (
CONF_ESP8266_DISABLE_SSL_SUPPORT,
CONF_ID,
CONF_PASSWORD,
CONF_TIMEOUT,
CONF_URL,
CONF_USERNAME,
)
from esphome.components import esp32
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
from esphome.core import CORE, coroutine_with_priority
from .. import http_request_ns
from esphome.core import coroutine_with_priority
from .. import CONF_HTTP_REQUEST_ID, http_request_ns, HttpRequestComponent
CODEOWNERS = ["@oarcher"]
AUTO_LOAD = ["md5"]
DEPENDENCIES = ["network"]
DEPENDENCIES = ["network", "http_request"]
CONF_MD5 = "md5"
CONF_MD5_URL = "md5_url"
CONF_VERIFY_SSL = "verify_ssl"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
OtaHttpRequestComponent = http_request_ns.class_(
"OtaHttpRequestComponent", OTAComponent
)
OtaHttpRequestComponentArduino = http_request_ns.class_(
"OtaHttpRequestComponentArduino", OtaHttpRequestComponent
)
OtaHttpRequestComponentIDF = http_request_ns.class_(
"OtaHttpRequestComponentIDF", OtaHttpRequestComponent
)
OtaHttpRequestComponentFlashAction = http_request_ns.class_(
"OtaHttpRequestComponentFlashAction", automation.Action
)
def validate_ssl_verification(config):
error_message = ""
if CORE.is_esp32:
if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
error_message = "ESPHome supports certificate verification only via ESP-IDF"
if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
error_message = "ESPHome does not support certificate verification in Arduino"
if (
CORE.is_esp8266
and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
and config[CONF_VERIFY_SSL]
):
error_message = "ESPHome does not support certificate verification in Arduino"
if len(error_message) > 0:
raise cv.Invalid(
f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
)
return config
def _declare_request_class(value):
if CORE.using_esp_idf:
return cv.declare_id(OtaHttpRequestComponentIDF)(value)
if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
return cv.declare_id(OtaHttpRequestComponentArduino)(value)
return NotImplementedError
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): _declare_request_class,
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
cv.only_on_esp8266, cv.boolean
),
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
cv.Optional(
CONF_TIMEOUT, default="5min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All(
cv.Any(cv.only_on_esp32, cv.only_on_rp2040),
cv.positive_not_null_time_period,
cv.positive_time_period_milliseconds,
),
cv.GenerateID(): cv.declare_id(OtaHttpRequestComponent),
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
}
)
.extend(BASE_OTA_SCHEMA)
@ -98,7 +41,6 @@ CONFIG_SCHEMA = cv.All(
esp_idf=cv.Version(0, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0),
),
validate_ssl_verification,
)
@ -106,41 +48,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ota_to_code(var, config)
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT):
cg.add_define(
"USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT",
timeout_ms,
)
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
if CORE.is_esp32:
if CORE.using_esp_idf:
esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
config.get(CONF_VERIFY_SSL),
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_INSECURE",
not config.get(CONF_VERIFY_SSL),
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
not config.get(CONF_VERIFY_SSL),
)
else:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("HTTPClient", None)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
@ -148,7 +57,9 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
{
cv.GenerateID(): cv.use_id(OtaHttpRequestComponent),
cv.Optional(CONF_MD5_URL): cv.templatable(cv.url),
cv.Optional(CONF_MD5): cv.templatable(cv.string),
cv.Optional(CONF_MD5): cv.templatable(
cv.All(cv.string, cv.Length(min=32, max=32))
),
cv.Optional(CONF_PASSWORD): cv.templatable(cv.string),
cv.Optional(CONF_USERNAME): cv.templatable(cv.string),
cv.Required(CONF_URL): cv.templatable(cv.url),
@ -159,7 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
@automation.register_action(
"ota_http_request.flash",
"ota.http_request.flash",
OtaHttpRequestComponentFlashAction,
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
)

View file

@ -1,45 +1,29 @@
#include "ota_http_request.h"
#include "watchdog.h"
#include "../watchdog.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/components/md5/md5.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_arduino_esp32.h"
#include "esphome/components/ota/ota_backend_arduino_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h"
#include "esphome/components/ota/ota_backend.h"
namespace esphome {
namespace http_request {
static const char *const TAG = "http_request.ota";
void OtaHttpRequestComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this);
#endif
}
void OtaHttpRequestComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request:");
ESP_LOGCONFIG(TAG, " Timeout: %llus", this->timeout_ / 1000);
#ifdef USE_ESP8266
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
ESP_LOGCONFIG(TAG, " ESP8266 SSL support: No");
#else
ESP_LOGCONFIG(TAG, " ESP8266 SSL support: Yes");
#endif
#endif
#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
ESP_LOGCONFIG(TAG, " TLS server verification: Yes");
#else
ESP_LOGCONFIG(TAG, " TLS server verification: No");
#endif
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
ESP_LOGCONFIG(TAG, " Watchdog timeout: %ds", USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT / 1000);
#endif
};
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
if (!this->validate_url_(url)) {
@ -58,20 +42,6 @@ void OtaHttpRequestComponent::set_url(const std::string &url) {
this->url_ = url;
}
bool OtaHttpRequestComponent::check_status() {
// status can be -1, or HTTP status code
if (this->status_ < 100) {
ESP_LOGE(TAG, "HTTP server did not respond (error %d)", this->status_);
return false;
}
if (this->status_ >= 310) {
ESP_LOGE(TAG, "HTTP error %d", this->status_);
return false;
}
ESP_LOGV(TAG, "HTTP status %d", this->status_);
return true;
}
void OtaHttpRequestComponent::flash() {
if (this->url_.empty()) {
ESP_LOGE(TAG, "URL not set; cannot start update");
@ -104,17 +74,18 @@ void OtaHttpRequestComponent::flash() {
}
}
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend) {
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
const std::shared_ptr<HttpContainer> &container) {
if (this->update_started_) {
ESP_LOGV(TAG, "Aborting OTA backend");
backend->abort();
}
ESP_LOGV(TAG, "Aborting HTTP connection");
this->http_end();
container->end();
};
uint8_t OtaHttpRequestComponent::do_ota_() {
uint8_t buf[this->http_recv_buffer_ + 1];
uint8_t buf[OtaHttpRequestComponent::HTTP_RECV_BUFFER + 1];
uint32_t last_progress = 0;
uint32_t update_start_time = millis();
md5::MD5Digest md5_receive;
@ -132,9 +103,10 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
}
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
this->http_init(url_with_auth);
if (!this->check_status()) {
this->http_end();
auto container = this->parent_->get(url_with_auth);
if (container == nullptr) {
return OTA_CONNECTION_ERROR;
}
@ -144,18 +116,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
ESP_LOGV(TAG, "OTA backend begin");
auto backend = ota::make_ota_backend();
auto error_code = backend->begin(this->body_length_);
auto error_code = backend->begin(container->content_length);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "backend->begin error: %d", error_code);
this->cleanup_(std::move(backend));
this->cleanup_(std::move(backend), container);
return error_code;
}
this->bytes_read_ = 0;
while (this->bytes_read_ < this->body_length_) {
while (container->get_bytes_read() < container->content_length) {
// read a maximum of chunk_size bytes into buf. (real read size returned)
int bufsize = this->http_read(buf, this->http_recv_buffer_);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", this->bytes_read_, this->body_length_, bufsize);
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
container->content_length, bufsize);
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
@ -163,9 +135,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
if (bufsize < 0) {
ESP_LOGE(TAG, "Stream closed");
this->cleanup_(std::move(backend));
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
} else if (bufsize > 0 && bufsize <= this->http_recv_buffer_) {
} else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// add read bytes to MD5
md5_receive.add(buf, bufsize);
@ -176,16 +148,16 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
this->bytes_read_ - bufsize, this->body_length_);
this->cleanup_(std::move(backend));
container->get_bytes_read() - bufsize, container->content_length);
this->cleanup_(std::move(backend), container);
return error_code;
}
}
uint32_t now = millis();
if ((now - last_progress > 1000) or (this->bytes_read_ == this->body_length_)) {
if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
last_progress = now;
float percentage = this->bytes_read_ * 100.0f / this->body_length_;
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
@ -201,13 +173,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
this->md5_computed_ = md5_receive_str.get();
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->cleanup_(std::move(backend));
this->cleanup_(std::move(backend), container);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
backend->set_update_md5(md5_receive_str.get());
}
this->http_end();
container->end();
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
@ -217,7 +189,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
error_code = backend->end();
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
this->cleanup_(std::move(backend));
this->cleanup_(std::move(backend), container);
return error_code;
}
@ -256,28 +228,32 @@ bool OtaHttpRequestComponent::http_get_md5_() {
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
this->http_init(url_with_auth);
if (!this->check_status()) {
this->http_end();
auto container = this->parent_->get(url_with_auth);
if (container == nullptr) {
ESP_LOGE(TAG, "Failed to connect to MD5 URL");
return false;
}
int length = this->body_length_;
if (length < 0) {
this->http_end();
size_t length = container->content_length;
if (length == 0) {
container->end();
return false;
}
if (length < MD5_SIZE) {
ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE,
this->body_length_);
this->http_end();
ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
container->end();
return false;
}
this->bytes_read_ = 0;
this->md5_expected_.resize(MD5_SIZE);
auto read_len = this->http_read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
this->http_end();
int read_len = 0;
while (container->get_bytes_read() < MD5_SIZE) {
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
App.feed_wdt();
yield();
}
container->end();
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
return read_len == MD5_SIZE;
}

View file

@ -1,17 +1,19 @@
#pragma once
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/helpers.h"
#include <memory>
#include <string>
#include <utility>
#include "../http_request.h"
namespace esphome {
namespace http_request {
static const char *const TAG = "http_request.ota";
static const uint8_t MD5_SIZE = 32;
enum OtaHttpRequestError : uint8_t {
@ -20,7 +22,7 @@ enum OtaHttpRequestError : uint8_t {
OTA_CONNECTION_ERROR = 0x12,
};
class OtaHttpRequestComponent : public ota::OTAComponent {
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
public:
void setup() override;
void dump_config() override;
@ -29,27 +31,19 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
void set_md5_url(const std::string &md5_url);
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
void set_password(const std::string &password) { this->password_ = password; }
void set_timeout(const uint64_t timeout) { this->timeout_ = timeout; }
void set_url(const std::string &url);
void set_username(const std::string &username) { this->username_ = username; }
std::string md5_computed() { return this->md5_computed_; }
std::string md5_expected() { return this->md5_expected_; }
bool check_status();
void flash();
virtual void http_init(const std::string &url){};
virtual int http_read(uint8_t *buf, size_t len) { return 0; };
virtual void http_end(){};
protected:
void cleanup_(std::unique_ptr<ota::OTABackend> backend);
void cleanup_(std::unique_ptr<ota::OTABackend> backend, const std::shared_ptr<HttpContainer> &container);
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();
bool secure_() { return this->url_.find("https:") != std::string::npos; };
bool validate_url_(const std::string &url);
std::string md5_computed_{};
@ -58,14 +52,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
std::string password_{};
std::string username_{};
std::string url_{};
size_t body_length_ = 0;
size_t bytes_read_ = 0;
int status_ = -1;
uint64_t timeout_ = 0;
bool update_started_ = false;
const uint16_t http_recv_buffer_ = 256; // the firmware GET chunk size
const uint16_t max_http_recv_buffer_ = 512; // internal max http buffer size must be > HTTP_RECV_BUFFER_ (TLS
// overhead) and must be a power of two from 512 to 4096
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
};
} // namespace http_request

View file

@ -1,134 +0,0 @@
#include "ota_http_request.h"
#include "watchdog.h"
#ifdef USE_ARDUINO
#include "ota_http_request_arduino.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/network/util.h"
#include "esphome/components/md5/md5.h"
namespace esphome {
namespace http_request {
struct Header {
const char *name;
const char *value;
};
void OtaHttpRequestComponentArduino::http_init(const std::string &url) {
const char *header_keys[] = {"Content-Length", "Content-Type"};
const size_t header_count = sizeof(header_keys) / sizeof(header_keys[0]);
watchdog::WatchdogManager wdts;
#ifdef USE_ESP8266
if (this->stream_ptr_ == nullptr && this->set_stream_ptr_()) {
ESP_LOGE(TAG, "Unable to set client");
return;
}
#endif // USE_ESP8266
#ifdef USE_RP2040
this->client_.setInsecure();
#endif
App.feed_wdt();
#if defined(USE_ESP32) || defined(USE_RP2040)
this->status_ = this->client_.begin(url.c_str());
#endif
#ifdef USE_ESP8266
this->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
this->status_ = this->client_.begin(*this->stream_ptr_, url.c_str());
#endif
if (!this->status_) {
this->client_.end();
return;
}
this->client_.setReuse(true);
// returned needed headers must be collected before the requests
this->client_.collectHeaders(header_keys, header_count);
// HTTP GET
this->status_ = this->client_.GET();
this->body_length_ = (size_t) this->client_.getSize();
#if defined(USE_ESP32) || defined(USE_RP2040)
if (this->stream_ptr_ == nullptr) {
this->set_stream_ptr_();
}
#endif
}
int OtaHttpRequestComponentArduino::http_read(uint8_t *buf, const size_t max_len) {
#ifdef USE_ESP8266
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?)
if (!this->secure_()) {
ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 "
"in your YAML, or use HTTPS");
}
#endif // USE_ARDUINO_VERSION_CODE
#endif // USE_ESP8266
watchdog::WatchdogManager wdts;
// Since arduino8266 >= 3.1 using this->stream_ptr_ is broken (https://github.com/esp8266/Arduino/issues/9035)
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return -1;
}
int available_data = stream_ptr->available();
int bufsize = std::min((int) max_len, available_data);
if (bufsize > 0) {
stream_ptr->readBytes(buf, bufsize);
this->bytes_read_ += bufsize;
buf[bufsize] = '\0'; // not fed to ota
}
return bufsize;
}
void OtaHttpRequestComponentArduino::http_end() {
watchdog::WatchdogManager wdts;
this->client_.end();
}
int OtaHttpRequestComponentArduino::set_stream_ptr_() {
#ifdef USE_ESP8266
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
if (this->secure_()) {
ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
this->stream_ptr_ = std::make_unique<WiFiClientSecure>();
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(this->stream_ptr_.get());
secure_client->setBufferSizes(this->max_http_recv_buffer_, 512);
secure_client->setInsecure();
} else {
this->stream_ptr_ = std::make_unique<WiFiClient>();
}
#else
ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient");
if (this->secure_()) {
ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support");
return -1;
}
this->stream_ptr_ = std::make_unique<WiFiClient>();
#endif // USE_HTTP_REQUEST_ESP8266_HTTPS
#endif // USE_ESP8266
#if defined(USE_ESP32) || defined(USE_RP2040)
this->stream_ptr_ = std::unique_ptr<WiFiClient>(this->client_.getStreamPtr());
#endif
return 0;
}
} // namespace http_request
} // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,42 +0,0 @@
#pragma once
#include "ota_http_request.h"
#ifdef USE_ARDUINO
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include <memory>
#include <string>
#include <utility>
#if defined(USE_ESP32) || defined(USE_RP2040)
#include <HTTPClient.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
#include <WiFiClientSecure.h>
#endif
#endif
namespace esphome {
namespace http_request {
class OtaHttpRequestComponentArduino : public OtaHttpRequestComponent {
public:
void http_init(const std::string &url) override;
int http_read(uint8_t *buf, size_t len) override;
void http_end() override;
protected:
int set_stream_ptr_();
HTTPClient client_{};
std::unique_ptr<WiFiClient> stream_ptr_;
};
} // namespace http_request
} // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,86 +0,0 @@
#include "ota_http_request_idf.h"
#include "watchdog.h"
#ifdef USE_ESP_IDF
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/components/md5/md5.h"
#include "esphome/components/network/util.h"
#include "esp_event.h"
#include "esp_http_client.h"
#include "esp_idf_version.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_system.h"
#include "esp_task_wdt.h"
#include "esp_tls.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include <cctype>
#include <cinttypes>
#include <cstdlib>
#include <cstring>
#include <sys/param.h>
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
#endif
namespace esphome {
namespace http_request {
void OtaHttpRequestComponentIDF::http_init(const std::string &url) {
App.feed_wdt();
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
esp_http_client_config_t config = {nullptr};
config.url = url.c_str();
config.method = HTTP_METHOD_GET;
config.timeout_ms = (int) this->timeout_;
config.buffer_size = this->max_http_recv_buffer_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
config.max_authorization_retries = -1;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (this->secure_()) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
#endif
#pragma GCC diagnostic pop
watchdog::WatchdogManager wdts;
this->client_ = esp_http_client_init(&config);
if ((this->status_ = esp_http_client_open(this->client_, 0)) == ESP_OK) {
this->body_length_ = esp_http_client_fetch_headers(this->client_);
this->status_ = esp_http_client_get_status_code(this->client_);
}
}
int OtaHttpRequestComponentIDF::http_read(uint8_t *buf, const size_t max_len) {
watchdog::WatchdogManager wdts;
int bufsize = std::min(max_len, this->body_length_ - this->bytes_read_);
App.feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
if (read_len > 0) {
this->bytes_read_ += bufsize;
buf[bufsize] = '\0'; // not fed to ota
}
return read_len;
}
void OtaHttpRequestComponentIDF::http_end() {
watchdog::WatchdogManager wdts;
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
}
} // namespace http_request
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -1,24 +0,0 @@
#pragma once
#include "ota_http_request.h"
#ifdef USE_ESP_IDF
#include "esp_http_client.h"
namespace esphome {
namespace http_request {
class OtaHttpRequestComponentIDF : public OtaHttpRequestComponent {
public:
void http_init(const std::string &url) override;
int http_read(uint8_t *buf, size_t len) override;
void http_end() override;
protected:
esp_http_client_handle_t client_{};
};
} // namespace http_request
} // namespace esphome
#endif // USE_ESP_IDF

View file

@ -0,0 +1,44 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import update
from esphome.const import (
CONF_SOURCE,
)
from .. import http_request_ns, CONF_HTTP_REQUEST_ID, HttpRequestComponent
from ..ota import OtaHttpRequestComponent
AUTO_LOAD = ["json"]
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["ota.http_request"]
HttpRequestUpdate = http_request_ns.class_(
"HttpRequestUpdate", update.UpdateEntity, cg.PollingComponent
)
CONF_OTA_ID = "ota_id"
CONFIG_SCHEMA = update.UPDATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(HttpRequestUpdate),
cv.GenerateID(CONF_OTA_ID): cv.use_id(OtaHttpRequestComponent),
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_SOURCE): cv.url,
}
).extend(cv.polling_component_schema("6h"))
async def to_code(config):
var = await update.new_update(config)
ota_parent = await cg.get_variable(config[CONF_OTA_ID])
cg.add(var.set_ota_parent(ota_parent))
request_parent = await cg.get_variable(config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_request_parent(request_parent))
cg.add(var.set_source_url(config[CONF_SOURCE]))
cg.add_define("USE_OTA_STATE_CALLBACK")
await cg.register_component(var, config)

View file

@ -0,0 +1,157 @@
#include "http_request_update.h"
#include "esphome/core/application.h"
#include "esphome/core/version.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
namespace esphome {
namespace http_request {
static const char *const TAG = "http_request.update";
static const size_t MAX_READ_SIZE = 256;
void HttpRequestUpdate::setup() {
this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) {
if (state == ota::OTAState::OTA_IN_PROGRESS) {
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = true;
this->update_info_.progress = progress;
this->publish_state();
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error("Failed to install firmware");
this->publish_state();
}
});
}
void HttpRequestUpdate::update() {
auto container = this->request_parent_->get(this->source_url_);
if (container == nullptr) {
std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str());
this->status_set_error(msg.c_str());
return;
}
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length);
this->status_set_error(msg.c_str());
container->end();
return;
}
size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
App.feed_wdt();
yield();
read_index += read_bytes;
}
std::string response((char *) data, read_index);
allocator.deallocate(data, container->content_length);
container->end();
bool valid = json::parse_json(response, [this](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
this->update_info_.title = root["name"].as<std::string>();
this->update_info_.latest_version = root["version"].as<std::string>();
for (auto build : root["builds"].as<JsonArray>()) {
if (!build.containsKey("chipFamily")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!build.containsKey("ota")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
auto ota = build["ota"];
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
this->update_info_.firmware_url = ota["path"].as<std::string>();
this->update_info_.md5 = ota["md5"].as<std::string>();
if (ota.containsKey("summary"))
this->update_info_.summary = ota["summary"].as<std::string>();
if (ota.containsKey("release_url"))
this->update_info_.release_url = ota["release_url"].as<std::string>();
return true;
}
}
return false;
});
if (!valid) {
std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str());
this->status_set_error(msg.c_str());
return;
}
// Merge source_url_ and this->update_info_.firmware_url
if (this->update_info_.firmware_url.find("http") == std::string::npos) {
std::string path = this->update_info_.firmware_url;
if (path[0] == '/') {
std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8));
this->update_info_.firmware_url = domain + path;
} else {
std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1);
this->update_info_.firmware_url = domain + path;
}
}
std::string current_version = this->current_version_;
if (current_version.empty()) {
#ifdef ESPHOME_PROJECT_VERSION
current_version = ESPHOME_PROJECT_VERSION;
#else
current_version = ESPHOME_VERSION;
#endif
}
this->update_info_.current_version = current_version;
if (this->update_info_.latest_version.empty()) {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
} else if (this->update_info_.latest_version != this->current_version_) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
}
this->update_info_.has_progress = false;
this->update_info_.progress = 0.0f;
this->status_clear_error();
this->publish_state();
}
void HttpRequestUpdate::perform() {
if (this->state_ != update::UPDATE_STATE_AVAILABLE) {
return;
}
this->state_ = update::UPDATE_STATE_INSTALLING;
this->publish_state();
this->ota_parent_->set_md5(this->update_info.md5);
this->ota_parent_->set_url(this->update_info.firmware_url);
// Flash in the next loop
this->defer([this]() { this->ota_parent_->flash(); });
}
} // namespace http_request
} // namespace esphome

View file

@ -0,0 +1,37 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/http_request/ota/ota_http_request.h"
#include "esphome/components/update/update_entity.h"
namespace esphome {
namespace http_request {
class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
public:
void setup() override;
void update() override;
void perform() override;
void set_source_url(const std::string &source_url) { this->source_url_ = source_url; }
void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; }
void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; }
void set_current_version(const std::string &current_version) { this->current_version_ = current_version; }
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
protected:
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;
std::string source_url_;
std::string current_version_{""};
};
} // namespace http_request
} // namespace esphome

View file

@ -1,7 +1,5 @@
#include "watchdog.h"
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@ -20,14 +18,22 @@ namespace esphome {
namespace http_request {
namespace watchdog {
static const char *const TAG = "watchdog.http_request.ota";
static const char *const TAG = "http_request.watchdog";
WatchdogManager::WatchdogManager() {
WatchdogManager::WatchdogManager(uint32_t timeout_ms) : timeout_ms_(timeout_ms) {
if (timeout_ms == 0) {
return;
}
this->saved_timeout_ms_ = this->get_timeout_();
this->set_timeout_(USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT);
this->set_timeout_(timeout_ms);
}
WatchdogManager::~WatchdogManager() { this->set_timeout_(this->saved_timeout_ms_); }
WatchdogManager::~WatchdogManager() {
if (this->timeout_ms_ == 0) {
return;
}
this->set_timeout_(this->saved_timeout_ms_);
}
void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms);
@ -68,4 +74,3 @@ uint32_t WatchdogManager::get_timeout_() {
} // namespace watchdog
} // namespace http_request
} // namespace esphome
#endif

View file

@ -9,9 +9,8 @@ namespace http_request {
namespace watchdog {
class WatchdogManager {
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
public:
WatchdogManager();
WatchdogManager(uint32_t timeout_ms);
~WatchdogManager();
private:
@ -19,7 +18,7 @@ class WatchdogManager {
void set_timeout_(uint32_t timeout_ms);
uint32_t saved_timeout_ms_{0};
#endif
uint32_t timeout_ms_{0};
};
} // namespace watchdog

View file

@ -27,43 +27,24 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
this->start();
}
}
if (play_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) {
this->is_announcement_ = true;
}
if (call.get_volume().has_value()) {
this->volume = call.get_volume().value();
this->set_volume_(volume);
this->unmute_();
}
if (this->i2s_state_ != I2S_STATE_RUNNING) {
return;
}
if (call.get_command().has_value()) {
switch (call.get_command().value()) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
if (!this->audio_->isRunning())
this->audio_->pauseResume();
this->state = play_state;
break;
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
if (this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
break;
case media_player::MEDIA_PLAYER_COMMAND_STOP:
this->stop();
break;
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
this->mute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
this->unmute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
this->audio_->pauseResume();
if (this->audio_->isRunning()) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
}
break;
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
float new_volume = this->volume + 0.1f;
if (new_volume > 1.0f)
@ -80,6 +61,36 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
this->unmute_();
break;
}
default:
break;
}
if (this->i2s_state_ != I2S_STATE_RUNNING) {
return;
}
switch (call.get_command().value()) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
if (!this->audio_->isRunning())
this->audio_->pauseResume();
this->state = play_state;
break;
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
if (this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
break;
case media_player::MEDIA_PLAYER_COMMAND_STOP:
this->stop();
break;
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
this->audio_->pauseResume();
if (this->audio_->isRunning()) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
}
break;
default:
break;
}
}
this->publish_state();
@ -171,9 +182,8 @@ void I2SAudioMediaPlayer::start_() {
if (this->current_url_.has_value()) {
this->audio_->connecttohost(this->current_url_.value().c_str());
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
if (this->is_announcement_.has_value()) {
this->state = this->is_announcement_.value() ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING
: media_player::MEDIA_PLAYER_STATE_PLAYING;
if (this->is_announcement_) {
this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING;
}
this->publish_state();
}
@ -202,6 +212,7 @@ void I2SAudioMediaPlayer::stop_() {
this->high_freq_.stop();
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
this->publish_state();
this->is_announcement_ = false;
}
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {

View file

@ -78,7 +78,7 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer,
HighFrequencyLoopRequester high_freq_;
optional<std::string> current_url_{};
optional<bool> is_announcement_{};
bool is_announcement_{false};
};
} // namespace i2s_audio

View file

@ -38,15 +38,22 @@ void I2SAudioSpeaker::start() {
ESP_LOGE(TAG, "Cannot start audio, speaker failed to setup");
return;
}
if (this->task_created_) {
ESP_LOGW(TAG, "Called start while task has been already created.");
return;
}
this->state_ = speaker::STATE_STARTING;
}
void I2SAudioSpeaker::start_() {
if (this->task_created_) {
return;
}
if (!this->parent_->try_lock()) {
return; // Waiting for another i2s component to return lock
}
this->state_ = speaker::STATE_RUNNING;
xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 1, &this->player_task_handle_);
this->task_created_ = true;
}
void I2SAudioSpeaker::player_task(void *params) {
@ -131,7 +138,16 @@ void I2SAudioSpeaker::player_task(void *params) {
(10 / portTICK_PERIOD_MS));
if (err != ESP_OK) {
event = {.type = TaskEventType::WARNING, .err = err};
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGW(TAG, "Failed to send WARNING event");
}
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--;
@ -139,18 +155,25 @@ void I2SAudioSpeaker::player_task(void *params) {
}
event.type = TaskEventType::PLAYING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
event.err = current;
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGW(TAG, "Failed to send PLAYING event");
}
}
event.type = TaskEventType::STOPPING;
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGW(TAG, "Failed to send STOPPING event");
}
i2s_zero_dma_buffer(this_speaker->parent_->get_port());
event.type = TaskEventType::STOPPING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
i2s_driver_uninstall(this_speaker->parent_->get_port());
event.type = TaskEventType::STOPPED;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGW(TAG, "Failed to send STOPPED event");
}
while (true) {
delay(10);
@ -181,6 +204,7 @@ void I2SAudioSpeaker::watch_() {
break;
case TaskEventType::STARTED:
ESP_LOGD(TAG, "Started I2S Audio Speaker");
this->state_ = speaker::STATE_RUNNING;
break;
case TaskEventType::STOPPING:
ESP_LOGD(TAG, "Stopping I2S Audio Speaker");
@ -191,6 +215,7 @@ void I2SAudioSpeaker::watch_() {
case TaskEventType::STOPPED:
this->state_ = speaker::STATE_STOPPED;
vTaskDelete(this->player_task_handle_);
this->task_created_ = false;
this->player_task_handle_ = nullptr;
this->parent_->unlock();
xQueueReset(this->buffer_queue_);
@ -208,7 +233,6 @@ void I2SAudioSpeaker::loop() {
switch (this->state_) {
case speaker::STATE_STARTING:
this->start_();
break;
case speaker::STATE_RUNNING:
case speaker::STATE_STOPPING:
this->watch_();

View file

@ -60,7 +60,6 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
protected:
void start_();
// void stop_();
void watch_();
static void player_task(void *params);
@ -70,6 +69,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
QueueHandle_t event_queue_;
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};

View file

@ -69,6 +69,7 @@ MODELS = {
"ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay),
"ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay),
"ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay),
"ST7735": ili9xxx_ns.class_("ILI9XXXST7735", ILI9XXXDisplay),
"ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay),
"ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay),
"S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay),
@ -134,6 +135,7 @@ def _validate(config):
"ILI9341",
"ILI9342",
"ST7789V",
"ST7735",
]:
raise cv.Invalid("Selected model can't run on ESP8266.")

View file

@ -70,6 +70,7 @@ static const uint8_t ILI9XXX_PWCTR2 = 0xC1;
static const uint8_t ILI9XXX_PWCTR3 = 0xC2;
static const uint8_t ILI9XXX_PWCTR4 = 0xC3;
static const uint8_t ILI9XXX_PWCTR5 = 0xC4;
static const uint8_t ILI9XXX_PWCTR6 = 0xF6;
static const uint8_t ILI9XXX_VMCTR1 = 0xC5;
static const uint8_t ILI9XXX_IFCTR = 0xC6;
static const uint8_t ILI9XXX_VMCTR2 = 0xC7;
@ -91,6 +92,7 @@ static const uint8_t ILI9XXX_GMCTRN1 = 0xE1;
static const uint8_t ILI9XXX_CSCON = 0xF0;
static const uint8_t ILI9XXX_ADJCTL3 = 0xF7;
static const uint8_t ILI9XXX_DELAY = 0xFF; // followed by one byte of delay time in ms
} // namespace ili9xxx
} // namespace esphome

View file

@ -34,8 +34,8 @@ void ILI9XXXDisplay::setup() {
ESP_LOGD(TAG, "Setting up ILI9xxx");
this->setup_pins_();
this->init_lcd_(this->init_sequence_);
this->init_lcd_(this->extra_init_sequence_.data());
this->init_lcd(this->init_sequence_);
this->init_lcd(this->extra_init_sequence_.data());
switch (this->pixel_mode_) {
case PIXEL_MODE_16:
if (this->is_18bitdisplay_) {
@ -405,7 +405,29 @@ void ILI9XXXDisplay::reset_() {
}
}
void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) {
void ILI9XXXDisplay::init_lcd(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;
while ((cmd = *addr++) != 0) {
x = *addr++;
if (cmd == ILI9XXX_DELAY) {
ESP_LOGD(TAG, "Delay %dms", x);
delay(x);
} else {
num_args = x & 0x7F;
ESP_LOGD(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, *addr);
this->send_command(cmd, addr, num_args);
addr += num_args;
if (x & 0x80) {
ESP_LOGD(TAG, "Delay 150ms");
delay(150); // NOLINT
}
}
}
}
void ILI9XXXGC9A01A::init_lcd(const uint8_t *addr) {
if (addr == nullptr)
return;
uint8_t cmd, x, num_args;

View file

@ -35,7 +35,6 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
while ((cmd = *addr++) != 0) {
num_args = *addr++ & 0x7F;
bits = *addr;
esph_log_d(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, bits);
switch (cmd) {
case ILI9XXX_MADCTL: {
this->swap_xy_ = (bits & MADCTL_MV) != 0;
@ -51,6 +50,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
break;
}
case ILI9XXX_DELAY:
continue; // no args to skip
default:
break;
}
@ -107,7 +109,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
virtual void set_madctl();
void display_();
void init_lcd_(const uint8_t *addr);
virtual void init_lcd(const uint8_t *addr);
void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2);
void reset_();
@ -267,6 +269,13 @@ class ILI9XXXS3BoxLite : public ILI9XXXDisplay {
class ILI9XXXGC9A01A : public ILI9XXXDisplay {
public:
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {}
void init_lcd(const uint8_t *addr) override;
};
//----------- ILI9XXX_24_TFT display --------------
class ILI9XXXST7735 : public ILI9XXXDisplay {
public:
ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160, false) {}
};
} // namespace ili9xxx

View file

@ -370,6 +370,57 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = {
0x00 // End of list
};
static const uint8_t PROGMEM INITCMD_ST7735[] = {
ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms
ILI9XXX_DELAY, 10,
ILI9XXX_SLPOUT , 0, // Exit Sleep, delay
ILI9XXX_DELAY, 10,
ILI9XXX_PIXFMT , 1, 0x05,
ILI9XXX_FRMCTR1, 3, // 4: Frame rate control, 3 args + delay:
0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D)
ILI9XXX_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args:
0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D)
ILI9XXX_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args:
0x01, 0x2C, 0x2D, // Dot inversion mode
0x01, 0x2C, 0x2D, // Line inversion mode
ILI9XXX_INVCTR, 1, // 7: Display inversion control, 1 arg:
0x7, // Line inversion
ILI9XXX_PWCTR1, 3, // 7: Power control, 3 args, no delay:
0xA2,
0x02, // -4.6V
0x84, // AUTO mode
ILI9XXX_PWCTR2, 1, // 8: Power control, 1 arg, no delay:
0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD
ILI9XXX_PWCTR3, 2, // 9: Power control, 2 args, no delay:
0x0A, // Opamp current small
0x00, // Boost frequency
ILI9XXX_PWCTR4, 2, // 10: Power control, 2 args, no delay:
0x8A, // BCLK/2,
0x2A, // opamp current small & medium low
ILI9XXX_PWCTR5, 2, // 11: Power control, 2 args, no delay:
0x8A, 0xEE,
ILI9XXX_VMCTR1, 1, // 11: Power control, 2 args + delay:
0x0E,
ILI9XXX_GMCTRP1, 16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay:
0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides
0x37, 0x32, 0x29, 0x2d, // accurate colors)
0x29, 0x25, 0x2B, 0x39,
0x00, 0x01, 0x03, 0x10,
ILI9XXX_GMCTRN1, 16, // 14: Gamma Adjustments (neg. polarity), 16 args + delay:
0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides
0x2E, 0x2C, 0x29, 0x2D, // accurate colors)
0x2E, 0x2E, 0x37, 0x3F,
0x00, 0x00, 0x02, 0x10,
ILI9XXX_MADCTL , 1, 0x00, // Memory Access Control, BGR
ILI9XXX_NORON , 0,
ILI9XXX_DELAY, 10,
ILI9XXX_DISPON , 0, // Display on
ILI9XXX_DELAY, 10,
00, // endo of list
};
// clang-format on
} // namespace ili9xxx
} // namespace esphome

View file

@ -9,8 +9,6 @@ import re
import requests
from magic import Magic
from PIL import Image
from esphome import core
from esphome.components import font
from esphome import external_files
@ -68,7 +66,7 @@ def _compute_local_icon_path(value: dict) -> Path:
return base_dir / f"{value[CONF_ICON]}.svg"
def _compute_local_image_path(value: dict) -> Path:
def compute_local_image_path(value: dict) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
@ -117,7 +115,7 @@ def download_mdi(value):
def download_image(value):
url = value[CONF_URL]
path = _compute_local_image_path(value)
path = compute_local_image_path(value)
download_content(url, path)
@ -267,6 +265,9 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
def load_svg_image(file: bytes, resize: tuple[int, int]):
# Local import only to allow "validate_pillow_installed" to run *before* importing it
from PIL import Image
# This import is only needed in case of SVG images; adding it
# to the top would force configurations not using SVG to also have it
# installed for no reason.
@ -286,6 +287,9 @@ def load_svg_image(file: bytes, resize: tuple[int, int]):
async def to_code(config):
# Local import only to allow "validate_pillow_installed" to run *before* importing it
from PIL import Image
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
@ -295,7 +299,7 @@ async def to_code(config):
path = _compute_local_icon_path(conf_file).as_posix()
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = _compute_local_image_path(conf_file).as_posix()
path = compute_local_image_path(conf_file).as_posix()
try:
with open(path, "rb") as f:

View file

@ -62,7 +62,7 @@ std::string build_json(const json_build_t &f) {
}
}
void parse_json(const std::string &data, const json_parse_t &f) {
bool parse_json(const std::string &data, const json_parse_t &f) {
// Here we are allocating 1.5 times the data size,
// with the heap size minus 2kb to be safe if less than that
// as we can not have a true dynamic sized document.
@ -76,14 +76,13 @@ void parse_json(const std::string &data, const json_parse_t &f) {
#elif defined(USE_LIBRETINY)
const size_t free_heap = lt_heap_get_free();
#endif
bool pass = false;
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
do {
while (true) {
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
free_heap);
return;
return false;
}
DeserializationError err = deserializeJson(json_document, data);
json_document.shrinkToFit();
@ -91,21 +90,21 @@ void parse_json(const std::string &data, const json_parse_t &f) {
JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) {
pass = true;
f(root);
return f(root);
} else if (err == DeserializationError::NoMemory) {
if (request_size * 2 >= free_heap) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return;
return false;
}
ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2;
continue;
} else {
ESP_LOGE(TAG, "JSON parse error: %s", err.c_str());
return;
return false;
}
} while (!pass);
};
return false;
}
} // namespace json

View file

@ -14,7 +14,7 @@ namespace esphome {
namespace json {
/// Callback function typedef for parsing JsonObjects.
using json_parse_t = std::function<void(JsonObject)>;
using json_parse_t = std::function<bool(JsonObject)>;
/// Callback function typedef for building JsonObjects.
using json_build_t = std::function<void(JsonObject)>;
@ -23,7 +23,7 @@ using json_build_t = std::function<void(JsonObject)>;
std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid.
void parse_json(const std::string &data, const json_parse_t &f);
bool parse_json(const std::string &data, const json_parse_t &f);
} // namespace json
} // namespace esphome

View file

@ -312,6 +312,7 @@ async def to_code(config):
esp32.add_idf_component(
name="esp-tflite-micro",
repo="https://github.com/espressif/esp-tflite-micro",
ref="v1.3.1",
)
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")

View file

@ -126,6 +126,7 @@ MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent)
MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent)
MQTTUpdateComponent = mqtt_ns.class_("MQTTUpdateComponent", MQTTComponent)
MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent)
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator")

View file

@ -410,7 +410,10 @@ void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t ca
void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) {
auto f = [callback](const std::string &topic, const std::string &payload) {
json::parse_json(payload, [topic, callback](JsonObject root) { callback(topic, root); });
json::parse_json(payload, [topic, callback](JsonObject root) -> bool {
callback(topic, root);
return true;
});
};
MQTTSubscription subscription{
.topic = topic,

View file

@ -137,6 +137,7 @@ constexpr const char *const MQTT_PAYLOAD_CLOSE = "pl_cls";
constexpr const char *const MQTT_PAYLOAD_DISARM = "pl_disarm";
constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd";
constexpr const char *const MQTT_PAYLOAD_HOME = "pl_home";
constexpr const char *const MQTT_PAYLOAD_INSTALL = "pl_inst";
constexpr const char *const MQTT_PAYLOAD_LOCATE = "pl_loc";
constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock";
constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd";
@ -396,6 +397,7 @@ constexpr const char *const MQTT_PAYLOAD_CLOSE = "payload_close";
constexpr const char *const MQTT_PAYLOAD_DISARM = "payload_disarm";
constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed";
constexpr const char *const MQTT_PAYLOAD_HOME = "payload_home";
constexpr const char *const MQTT_PAYLOAD_INSTALL = "payload_install";
constexpr const char *const MQTT_PAYLOAD_LOCATE = "payload_locate";
constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock";
constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed";

View file

@ -6,12 +6,12 @@
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
#ifdef USE_DATETIME_DATETIME
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.datetime.time";
static const char *const TAG = "mqtt.datetime.datetime";
using namespace esphome::datetime;
@ -80,5 +80,5 @@ bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_TIME
#endif // USE_DATETIME_DATETIME
#endif // USE_MQTT

View file

@ -3,7 +3,7 @@
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#include "mqtt_component.h"
@ -17,7 +17,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
*
* @param time The time entity.
*/
explicit MQTTDateTimeComponent(datetime::DateTimeEntity *time);
explicit MQTTDateTimeComponent(datetime::DateTimeEntity *datetime);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@ -41,5 +41,5 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_DATE
#endif // USE_DATETIME_DATETIME
#endif // USE_MQTT

View file

@ -0,0 +1,62 @@
#include "mqtt_update.h"
#include "esphome/core/log.h"
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_UPDATE
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.update";
using namespace esphome::update;
MQTTUpdateComponent::MQTTUpdateComponent(UpdateEntity *update) : update_(update) {}
void MQTTUpdateComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
if (payload == "INSTALL") {
this->update_->perform();
} else {
ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
}
});
this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
}
bool MQTTUpdateComponent::publish_state() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
root["installed_version"] = this->update_->update_info.current_version;
root["latest_version"] = this->update_->update_info.latest_version;
root["title"] = this->update_->update_info.title;
if (!this->update_->update_info.summary.empty())
root["release_summary"] = this->update_->update_info.summary;
if (!this->update_->update_info.release_url.empty())
root["release_url"] = this->update_->update_info.release_url;
});
}
void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
root["schema"] = "json";
root[MQTT_PAYLOAD_INSTALL] = "INSTALL";
}
bool MQTTUpdateComponent::send_initial_state() { return this->publish_state(); }
void MQTTUpdateComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Update '%s': ", this->update_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true);
}
std::string MQTTUpdateComponent::component_type() const { return "update"; }
const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; }
} // namespace mqtt
} // namespace esphome
#endif // USE_UPDATE
#endif // USE_MQTT

View file

@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_UPDATE
#include "esphome/components/update/update_entity.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTUpdateComponent : public mqtt::MQTTComponent {
public:
explicit MQTTUpdateComponent(update::UpdateEntity *update);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool publish_state();
protected:
/// "update" component type.
std::string component_type() const override;
const EntityBase *get_entity() const override;
update::UpdateEntity *update_;
};
} // namespace mqtt
} // namespace esphome
#endif // USE_UPDATE
#endif // USE_MQTT

View file

@ -0,0 +1,40 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS
CODEOWNERS = ["@ssieb"]
IS_PLATFORM_COMPONENT = True
CONF_ONE_WIRE_ID = "one_wire_id"
one_wire_ns = cg.esphome_ns.namespace("one_wire")
OneWireBus = one_wire_ns.class_("OneWireBus")
OneWireDevice = one_wire_ns.class_("OneWireDevice")
def one_wire_device_schema():
"""Create a schema for a 1-wire device.
:return: The 1-wire device schema, `extend` this in your config schema.
"""
schema = cv.Schema(
{
cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus),
cv.Optional(CONF_ADDRESS): cv.hex_uint64_t,
}
)
return schema
async def register_one_wire_device(var, config):
"""Register an 1-wire device with the given config.
Sets the 1-wire bus to use and the 1-wire address.
This is a coroutine, you need to await it with a 'yield' expression!
"""
parent = await cg.get_variable(config[CONF_ONE_WIRE_ID])
cg.add(var.set_one_wire_bus(parent))
if (address := config.get(CONF_ADDRESS)) is not None:
cg.add(var.set_address(address))

View file

@ -0,0 +1,40 @@
#include "one_wire.h"
namespace esphome {
namespace one_wire {
static const char *const TAG = "one_wire";
const std::string &OneWireDevice::get_address_name() {
if (this->address_name_.empty())
this->address_name_ = std::string("0x") + format_hex(this->address_);
return this->address_name_;
}
std::string OneWireDevice::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); }
bool OneWireDevice::send_command_(uint8_t cmd) {
if (!this->bus_->select(this->address_))
return false;
this->bus_->write8(cmd);
return true;
}
bool OneWireDevice::check_address_() {
if (this->address_ != 0)
return true;
auto devices = this->bus_->get_devices();
if (devices.empty()) {
ESP_LOGE(TAG, "No devices, can't auto-select address");
return false;
}
if (devices.size() > 1) {
ESP_LOGE(TAG, "More than one device, can't auto-select address");
return false;
}
this->address_ = devices[0];
return true;
}
} // namespace one_wire
} // namespace esphome

View file

@ -0,0 +1,44 @@
#pragma once
#include "one_wire_bus.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace one_wire {
#define LOG_ONE_WIRE_DEVICE(this) \
ESP_LOGCONFIG(TAG, " Address: %s (%s)", this->get_address_name().c_str(), \
LOG_STR_ARG(this->bus_->get_model_str(this->address_ & 0xff)));
class OneWireDevice {
public:
/// @brief store the address of the device
/// @param address of the device
void set_address(uint64_t address) { this->address_ = address; }
/// @brief store the pointer to the OneWireBus to use
/// @param bus pointer to the OneWireBus object
void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; }
/// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29".
const std::string &get_address_name();
std::string unique_id();
protected:
uint64_t address_{0};
OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance
std::string address_name_;
/// @brief find an address if necessary
/// should be called from setup
bool check_address_();
/// @brief send command on the bus
/// @param cmd command to send
bool send_command_(uint8_t cmd);
};
} // namespace one_wire
} // namespace esphome

View file

@ -0,0 +1,88 @@
#include "one_wire_bus.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace one_wire {
static const char *const TAG = "one_wire";
static const uint8_t DALLAS_MODEL_DS18S20 = 0x10;
static const uint8_t DALLAS_MODEL_DS1822 = 0x22;
static const uint8_t DALLAS_MODEL_DS18B20 = 0x28;
static const uint8_t DALLAS_MODEL_DS1825 = 0x3B;
static const uint8_t DALLAS_MODEL_DS28EA00 = 0x42;
const uint8_t ONE_WIRE_ROM_SELECT = 0x55;
const uint8_t ONE_WIRE_ROM_SEARCH = 0xF0;
const std::vector<uint64_t> &OneWireBus::get_devices() { return this->devices_; }
bool IRAM_ATTR OneWireBus::select(uint64_t address) {
if (!this->reset())
return false;
this->write8(ONE_WIRE_ROM_SELECT);
this->write64(address);
return true;
}
void OneWireBus::search() {
this->devices_.clear();
this->reset_search();
uint64_t address;
while (true) {
{
InterruptLock lock;
if (!this->reset()) {
// Reset failed or no devices present
return;
}
this->write8(ONE_WIRE_ROM_SEARCH);
address = this->search_int();
}
if (address == 0)
break;
auto *address8 = reinterpret_cast<uint8_t *>(&address);
if (crc8(address8, 7) != address8[7]) {
ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str());
} else {
this->devices_.push_back(address);
}
}
}
void OneWireBus::skip() {
this->write8(0xCC); // skip ROM
}
const LogString *OneWireBus::get_model_str(uint8_t model) {
switch (model) {
case DALLAS_MODEL_DS18S20:
return LOG_STR("DS18S20");
case DALLAS_MODEL_DS1822:
return LOG_STR("DS1822");
case DALLAS_MODEL_DS18B20:
return LOG_STR("DS18B20");
case DALLAS_MODEL_DS1825:
return LOG_STR("DS1825");
case DALLAS_MODEL_DS28EA00:
return LOG_STR("DS28EA00");
default:
return LOG_STR("Unknown");
}
}
void OneWireBus::dump_devices_(const char *tag) {
if (this->devices_.empty()) {
ESP_LOGW(tag, " Found no devices!");
} else {
ESP_LOGCONFIG(tag, " Found devices:");
for (auto &address : this->devices_) {
ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex(address).c_str(), LOG_STR_ARG(get_model_str(address & 0xff)));
}
}
}
} // namespace one_wire
} // namespace esphome

View file

@ -0,0 +1,61 @@
#pragma once
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <vector>
namespace esphome {
namespace one_wire {
class OneWireBus {
public:
/** Reset the bus, should be done before all write operations.
*
* Takes approximately 1ms.
*
* @return Whether the operation was successful.
*/
virtual bool reset() = 0;
/// Write a word to the bus. LSB first.
virtual void write8(uint8_t val) = 0;
/// Write a 64 bit unsigned integer to the bus. LSB first.
virtual void write64(uint64_t val) = 0;
/// Write a command to the bus that addresses all devices by skipping the ROM.
void skip();
/// Read an 8 bit word from the bus.
virtual uint8_t read8() = 0;
/// Read an 64-bit unsigned integer from the bus.
virtual uint64_t read64() = 0;
/// Select a specific address on the bus for the following command.
bool select(uint64_t address);
/// Return the list of found devices.
const std::vector<uint64_t> &get_devices();
/// Search for 1-Wire devices on the bus.
void search();
/// Get the description string for this model.
const LogString *get_model_str(uint8_t model);
protected:
std::vector<uint64_t> devices_;
/// log the found devices
void dump_devices_(const char *tag);
/// Reset the device search.
virtual void reset_search() = 0;
/// Search for a 1-Wire device on the bus. Returns 0 if all devices have been found.
virtual uint64_t search_int() = 0;
};
} // namespace one_wire
} // namespace esphome

View file

@ -47,10 +47,16 @@ def set_core_data(config):
def get_download_types(storage_json):
return [
{
"title": "UF2 format",
"title": "UF2 factory format",
"description": "For copying to RP2040 over USB.",
"file": "firmware.uf2",
"download": f"{storage_json.name}.uf2",
"download": f"{storage_json.name}.factory.uf2",
},
{
"title": "OTA format",
"description": "For OTA updating a device.",
"file": "firmware.ota.bin",
"download": f"{storage_json.name}.ota.bin",
},
]
@ -160,6 +166,8 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
@ -225,4 +233,10 @@ def generate_pio_files() -> bool:
# Called by writer.py
def copy_files() -> bool:
dir = os.path.dirname(__file__)
post_build_file = os.path.join(dir, "post_build.py.script")
copy_file_if_changed(
post_build_file,
CORE.relative_build_path("post_build.py"),
)
return generate_pio_files()

View file

@ -0,0 +1,23 @@
import shutil
# pylint: disable=E0602
Import("env") # noqa
def rp2040_copy_factory_uf2(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.uf2")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.uf2")
shutil.copyfile(firmware_name, new_file_name)
def rp2040_copy_ota_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", rp2040_copy_factory_uf2) # noqa
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", rp2040_copy_ota_bin) # noqa

View file

@ -16,6 +16,7 @@ from esphome import automation
CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"]
CONF_BOOT_IS_GOOD_AFTER = "boot_is_good_after"
CONF_ON_SAFE_MODE = "on_safe_mode"
safe_mode_ns = cg.esphome_ns.namespace("safe_mode")
@ -34,6 +35,9 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SafeModeComponent),
cv.Optional(
CONF_BOOT_IS_GOOD_AFTER, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_DISABLED, default=False): cv.boolean,
cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
cv.Optional(
@ -63,7 +67,9 @@ async def to_code(config):
await automation.build_automation(trigger, [], conf)
condition = var.should_enter_safe_mode(
config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT]
config[CONF_NUM_ATTEMPTS],
config[CONF_REBOOT_TIMEOUT],
config[CONF_BOOT_IS_GOOD_AFTER],
)
cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_SAFE_MODE] = {}

View file

@ -16,6 +16,8 @@ static const char *const TAG = "safe_mode";
void SafeModeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Safe Mode:");
ESP_LOGCONFIG(TAG, " Boot considered successful after %" PRIu32 " seconds",
this->safe_mode_boot_is_good_after_ / 1000); // because milliseconds
ESP_LOGCONFIG(TAG, " Invoke after %u boot attempts", this->safe_mode_num_attempts_);
ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds",
this->safe_mode_enable_time_ / 1000); // because milliseconds
@ -34,7 +36,7 @@ void SafeModeComponent::dump_config() {
float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void SafeModeComponent::loop() {
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
// successful boot, reset counter
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc();
@ -60,9 +62,11 @@ bool SafeModeComponent::get_safe_mode_pending() {
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
}
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time,
uint32_t boot_is_good_after) {
this->safe_mode_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time;
this->safe_mode_boot_is_good_after_ = boot_is_good_after;
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
this->safe_mode_rtc_value_ = this->read_rtc_();

View file

@ -11,7 +11,7 @@ namespace safe_mode {
/// SafeModeComponent provides a safe way to recover from repeated boot failures
class SafeModeComponent : public Component {
public:
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time);
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after);
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
@ -34,10 +34,11 @@ class SafeModeComponent : public Component {
uint32_t read_rtc_();
bool boot_successful_{false}; ///< set to true after boot is considered successful
uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled
uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful
uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for
uint32_t safe_mode_rtc_value_;
uint8_t safe_mode_num_attempts_;
uint32_t safe_mode_rtc_value_{0};
uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled
uint8_t safe_mode_num_attempts_{0};
ESPPreferenceObject rtc_;
CallbackManager<void()> safe_mode_callback_{};

View file

@ -0,0 +1 @@
CODEOWNERS = ["@clydebarrow"]

View file

@ -0,0 +1,72 @@
import subprocess
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display
from esphome.const import (
CONF_ID,
CONF_DIMENSIONS,
CONF_WIDTH,
CONF_HEIGHT,
CONF_LAMBDA,
PLATFORM_HOST,
)
sdl_ns = cg.esphome_ns.namespace("sdl")
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component)
CONF_SDL_OPTIONS = "sdl_options"
CONF_SDL_ID = "sdl_id"
def get_sdl_options(value):
if value != "":
return value
try:
return subprocess.check_output(["sdl2-config", "--cflags", "--libs"]).decode()
except Exception as e:
raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Sdl),
cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options,
cv.Required(CONF_DIMENSIONS): cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
}
),
),
}
)
),
cv.only_on(PLATFORM_HOST),
)
async def to_code(config):
for option in config[CONF_SDL_OPTIONS].split():
cg.add_build_flag(option)
cg.add_build_flag("-DSDL_BYTEORDER=4321")
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
else:
(width, height) = dimensions
cg.add(var.set_dimensions(width, height))
if lamb := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lamb, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))

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