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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -81,7 +81,8 @@ RUN \
fi; \ fi; \
pip3 install \ pip3 install \
--break-system-packages --no-cache-dir \ --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 # Change some platformio settings
&& platformio settings set enable_telemetry No \ && platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \ && 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 && /platformio_install_deps.py /platformio.ini --libraries
# Avoid unsafe git error when container user and file config volume permissions don't match # 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 ======================= # ======================= docker-type image =======================

View file

@ -488,6 +488,15 @@ def command_run(args, config):
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
_LOGGER.info("Successfully compiled program.") _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( port = choose_upload_log_host(
default=args.device, default=args.device,
check_default=None, check_default=None,

View file

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

View file

@ -3,7 +3,13 @@ import logging
from esphome import automation, core from esphome import automation, core
from esphome.components import font from esphome.components import font
import esphome.components.image as espImage import esphome.components.image as espImage
from esphome.components.image import 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.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import ( from esphome.const import (
@ -13,6 +19,9 @@ from esphome.const import (
CONF_REPEAT, CONF_REPEAT,
CONF_RESIZE, CONF_RESIZE,
CONF_TYPE, CONF_TYPE,
CONF_SOURCE,
CONF_PATH,
CONF_URL,
) )
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
@ -43,6 +52,40 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) "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): def validate_cross_dependencies(config):
""" """
@ -67,7 +110,7 @@ ANIMATION_SCHEMA = cv.Schema(
cv.All( cv.All(
{ {
cv.Required(CONF_ID): cv.declare_id(Animation_), 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_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True 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): async def to_code(config):
from PIL import Image 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: try:
image = Image.open(path) image = Image.open(path)
except Exception as e: except Exception as e:

View file

@ -48,6 +48,7 @@ service APIConnection {
rpc date_command (DateCommandRequest) returns (void) {} rpc date_command (DateCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {} rpc time_command (TimeCommandRequest) returns (void) {}
rpc datetime_command (DateTimeCommandRequest) returns (void) {} rpc datetime_command (DateTimeCommandRequest) returns (void) {}
rpc update_command (UpdateCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@ -1837,3 +1838,46 @@ message DateTimeCommandRequest {
fixed32 key = 1; fixed32 key = 1;
fixed32 epoch_seconds = 2; 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 #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) { bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
if (this->log_subscription_ < level) if (this->log_subscription_ < level)
return false; return false;

View file

@ -164,6 +164,12 @@ class APIConnection : public APIServerConnection {
bool send_event_info(event::Event *event); bool send_event_info(event::Event *event);
#endif #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_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override { void on_ping_response(const PingResponse &value) override {
// we initiated ping // we initiated ping

View file

@ -8376,6 +8376,262 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #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 api
} // namespace esphome } // namespace esphome

View file

@ -2130,6 +2130,61 @@ class DateTimeCommandRequest : public ProtoMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; 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 api
} // namespace esphome } // namespace esphome

View file

@ -611,6 +611,24 @@ bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateR
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
#endif #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) { bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) { switch (msg_type) {
case 1: { 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()); ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str());
#endif #endif
this->on_voice_assistant_timer_event_response(msg); 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 #endif
break; break;
} }
@ -1434,6 +1463,19 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
this->datetime_command(msg); this->datetime_command(msg);
} }
#endif #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 #ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) { const SubscribeBluetoothLEAdvertisementsRequest &msg) {

View file

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

View file

@ -334,6 +334,13 @@ void APIServer::on_event(event::Event *obj, const std::string &event_type) {
} }
#endif #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; } float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; } void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 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 #ifdef USE_EVENT
void on_event(event::Event *obj, const std::string &event_type) override; void on_event(event::Event *obj, const std::string &event_type) override;
#endif #endif
#ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override;
#endif
bool is_connected() const; bool is_connected() const;

View file

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

View file

@ -75,6 +75,9 @@ class ListEntitiesIterator : public ComponentIterator {
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *event) override; bool on_event(event::Event *event) override;
#endif
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override;
#endif #endif
bool on_end() override; 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); return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
} }
#endif #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) {} InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api } // namespace api

View file

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

View file

@ -6,18 +6,24 @@ namespace climate_ir_lg {
static const char *const TAG = "climate.climate_ir_lg"; static const char *const TAG = "climate.climate_ir_lg";
const uint32_t COMMAND_ON = 0x00000; // Commands
const uint32_t COMMAND_ON_AI = 0x03000; const uint32_t COMMAND_MASK = 0xFF000;
const uint32_t COMMAND_COOL = 0x08000;
const uint32_t COMMAND_HEAT = 0x0C000;
const uint32_t COMMAND_OFF = 0xC0000; const uint32_t COMMAND_OFF = 0xC0000;
const uint32_t COMMAND_SWING = 0x10000; 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_MASK = 0xF0;
const uint32_t FAN_AUTO = 0x50; const uint32_t FAN_AUTO = 0x50;
const uint32_t FAN_MIN = 0x00; const uint32_t FAN_MIN = 0x00;
@ -35,69 +41,67 @@ void LgIrClimate::transmit_state() {
uint32_t remote_state = 0x8800000; uint32_t remote_state = 0x8800000;
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
// Set command
if (send_swing_cmd_) { if (send_swing_cmd_) {
send_swing_cmd_ = false; send_swing_cmd_ = false;
remote_state |= COMMAND_SWING; remote_state |= COMMAND_SWING;
} else { } else {
if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF);
remote_state |= COMMAND_ON_AI; switch (this->mode) {
} else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { case climate::CLIMATE_MODE_COOL:
remote_state |= COMMAND_ON; remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
this->mode = climate::CLIMATE_MODE_COOL; break;
} else { case climate::CLIMATE_MODE_DRY:
switch (this->mode) { remote_state |= climate_is_off ? COMMAND_ON_DRY : COMMAND_DRY;
case climate::CLIMATE_MODE_COOL: break;
remote_state |= COMMAND_COOL; case climate::CLIMATE_MODE_FAN_ONLY:
break; remote_state |= climate_is_off ? COMMAND_ON_FAN_ONLY : COMMAND_FAN_ONLY;
case climate::CLIMATE_MODE_HEAT: break;
remote_state |= COMMAND_HEAT; case climate::CLIMATE_MODE_HEAT_COOL:
break; remote_state |= climate_is_off ? COMMAND_ON_AI : COMMAND_AI;
case climate::CLIMATE_MODE_HEAT_COOL: break;
remote_state |= COMMAND_AUTO; case climate::CLIMATE_MODE_HEAT:
break; remote_state |= climate_is_off ? COMMAND_ON_HEAT : COMMAND_HEAT;
case climate::CLIMATE_MODE_DRY: break;
remote_state |= COMMAND_DRY_FAN; case climate::CLIMATE_MODE_OFF:
break; default:
case climate::CLIMATE_MODE_OFF: remote_state |= COMMAND_OFF;
default: break;
remote_state |= COMMAND_OFF;
break;
}
}
mode_before_ = this->mode;
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
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) {
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_HIGH:
remote_state |= FAN_MAX;
break;
case climate::CLIMATE_FAN_MEDIUM:
remote_state |= FAN_MED;
break;
case climate::CLIMATE_FAN_LOW:
remote_state |= FAN_MIN;
break;
case climate::CLIMATE_FAN_AUTO:
default:
remote_state |= FAN_AUTO;
break;
}
}
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
// remote_state |= FAN_MODE_AUTO_DRY;
}
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);
} }
} }
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 {
switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_HIGH:
remote_state |= FAN_MAX;
break;
case climate::CLIMATE_FAN_MEDIUM:
remote_state |= FAN_MED;
break;
case climate::CLIMATE_FAN_LOW:
remote_state |= FAN_MIN;
break;
case climate::CLIMATE_FAN_AUTO:
default:
remote_state |= FAN_AUTO;
break;
}
}
// 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); transmit_(remote_state);
this->publish_state(); this->publish_state();
} }
@ -125,37 +129,42 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
if ((remote_state & 0xFF00000) != 0x8800000) if ((remote_state & 0xFF00000) != 0x8800000)
return false; return false;
if ((remote_state & COMMAND_MASK) == COMMAND_ON) { // Get command
this->mode = climate::CLIMATE_MODE_COOL;
} else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
}
if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { if ((remote_state & COMMAND_MASK) == COMMAND_OFF) {
this->mode = climate::CLIMATE_MODE_OFF; this->mode = climate::CLIMATE_MODE_OFF;
} else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) { } else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) {
this->swing_mode = this->swing_mode =
this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
} else { } else {
if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) { switch (remote_state & COMMAND_MASK) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL; case COMMAND_DRY:
} else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) { case COMMAND_ON_DRY:
this->mode = climate::CLIMATE_MODE_DRY; this->mode = climate::CLIMATE_MODE_DRY;
} else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) { break;
this->mode = climate::CLIMATE_MODE_HEAT; case COMMAND_FAN_ONLY:
} else { case COMMAND_ON_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_COOL; 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;
break;
case COMMAND_COOL:
case COMMAND_ON_COOL:
default:
this->mode = climate::CLIMATE_MODE_COOL;
break;
} }
// Temperature // Get fan speed
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
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) { if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
this->fan_mode = climate::CLIMATE_FAN_AUTO; this->fan_mode = climate::CLIMATE_FAN_AUTO;
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT || } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
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) { if ((remote_state & FAN_MASK) == FAN_AUTO) {
this->fan_mode = climate::CLIMATE_FAN_AUTO; this->fan_mode = climate::CLIMATE_FAN_AUTO;
} else if ((remote_state & FAN_MASK) == FAN_MIN) { } 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; 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(); this->publish_state();
return true; return true;
} }
void LgIrClimate::transmit_(uint32_t value) { void LgIrClimate::transmit_(uint32_t value) {
calc_checksum_(value); calc_checksum_(value);
ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, 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 { class LgIrClimate : public climate_ir::ClimateIR {
public: public:
LgIrClimate() 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_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH}, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} {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 import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_PIN
MULTI_CONF = True MULTI_CONF = True
AUTO_LOAD = ["sensor"]
dallas_ns = cg.esphome_ns.namespace("dallas") CONFIG_SCHEMA = cv.invalid(
DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent) 'The "dallas" component has been replaced by the "one_wire" component.\nhttps://esphome.io/components/one_wire'
)
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))

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 import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import ( CONFIG_SCHEMA = cv.invalid(
CONF_ADDRESS, 'The "dallas" sensor is now "dallas_temp"\nhttps://esphome.io/components/sensor/dallas_temp'
CONF_DALLAS_ID,
CONF_INDEX,
CONF_RESOLUTION,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
) )
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() { void DateCall::perform() {
this->validate_(); 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); this->parent_->control(*this);
} }

View file

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

View file

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

View file

@ -17,17 +17,19 @@ from SCons.Script import ARGUMENTS
# Copy over the default sdkconfig. # Copy over the default sdkconfig.
from os import path from os import path
if path.exists("./sdkconfig.defaults"): if path.exists("./sdkconfig.defaults"):
os.makedirs(".temp", exist_ok=True) os.makedirs(".temp", exist_ok=True)
shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf")
def esp32_create_combined_bin(source, target, env): def esp32_create_combined_bin(source, target, env):
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0")))
if verbose: if verbose:
print("Generating combined binary for serial flashing") print("Generating combined binary for serial flashing")
app_offset = 0x10000 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")) sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU") chip = env.get("BOARD_MCU")
@ -62,5 +64,14 @@ def esp32_create_combined_bin(source, target, env):
else: else:
subprocess.run(["esptool.py", *cmd]) 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 # pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa 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, CONF_VSYNC_PIN,
) )
from esphome.core import CORE 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 from esphome.cpp_helpers import setup_entity
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@ -290,8 +290,11 @@ async def to_code(config):
cg.add_define("USE_ESP32_CAMERA") cg.add_define("USE_ESP32_CAMERA")
if CORE.using_esp_idf: if CORE.using_esp_idf:
cg.add_library("espressif/esp32-camera", "1.0.0") add_idf_component(
add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) name="esp32-camera",
repo="https://github.com/espressif/esp32-camera.git",
ref="v2.0.9",
)
for conf in config.get(CONF_ON_STREAM_START, []): for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 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): def esp8266_copy_factory_bin(source, target, env):
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") 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) shutil.copyfile(firmware_name, new_file_name)
# pylint: disable=E0602 # pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp8266_copy_factory_bin) # noqa 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"); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { 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); 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"); 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->position = new_position;
this->current_operation = COVER_OPERATION_IDLE; this->current_operation = COVER_OPERATION_IDLE;
if (this->last_command_ == operation) { 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(), ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur); operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
} }
@ -69,7 +69,6 @@ void HE60rCover::set_current_operation_(cover::CoverOperation operation) {
this->current_operation = operation; this->current_operation = operation;
if (operation != COVER_OPERATION_IDLE) if (operation != COVER_OPERATION_IDLE)
this->last_recompute_time_ = millis(); this->last_recompute_time_ = millis();
this->publish_state();
} }
} }
@ -129,7 +128,7 @@ void HE60rCover::update_() {
if (this->toggles_needed_ != 0) { if (this->toggles_needed_ != 0) {
if ((this->counter_++ & 0x3) == 0) { if ((this->counter_++ & 0x3) == 0) {
this->toggles_needed_--; 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); this->write_byte(TOGGLE_BYTE);
} else { } else {
this->write_byte(QUERY_BYTE); this->write_byte(QUERY_BYTE);
@ -235,31 +234,28 @@ void HE60rCover::recompute_position_() {
return; return;
const uint32_t now = millis(); const uint32_t now = millis();
float dir;
float action_dur;
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
dir = 1.0f;
action_dur = this->open_duration_;
break;
case COVER_OPERATION_CLOSING:
dir = -1.0f;
action_dur = this->close_duration_;
break;
default:
return;
}
if (now > this->last_recompute_time_) { if (now > this->last_recompute_time_) {
auto diff = now - last_recompute_time_; auto diff = (unsigned) (now - last_recompute_time_);
auto delta = dir * diff / action_dur; float delta;
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
delta = (float) diff / (float) this->open_duration_;
break;
case COVER_OPERATION_CLOSING:
delta = -(float) diff / (float) this->close_duration_;
break;
default:
return;
}
// make sure our guesstimate never reaches full open or close. // make sure our guesstimate never reaches full open or close.
this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f); auto new_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, ESP_LOGD(TAG, "Recompute %ums, dir=%u, delta=%f, pos=%f", diff, this->current_operation, delta, new_position);
this->position);
this->last_recompute_time_ = now; this->last_recompute_time_ = now;
this->publish_state(); if (this->position != new_position) {
this->position = new_position;
this->publish_state();
}
} }
} }

View file

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

View file

@ -1,9 +1,8 @@
import urllib.parse as urlparse
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.const import ( from esphome.const import (
__version__,
CONF_ID, CONF_ID,
CONF_TIMEOUT, CONF_TIMEOUT,
CONF_METHOD, CONF_METHOD,
@ -12,67 +11,91 @@ from esphome.const import (
CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ESP8266_DISABLE_SSL_SUPPORT,
) )
from esphome.core import Lambda, CORE from esphome.core import Lambda, CORE
from esphome.components import esp32
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["json"] AUTO_LOAD = ["json"]
http_request_ns = cg.esphome_ns.namespace("http_request") http_request_ns = cg.esphome_ns.namespace("http_request")
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component) 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 = http_request_ns.class_(
"HttpRequestSendAction", automation.Action "HttpRequestSendAction", automation.Action
) )
HttpRequestResponseTrigger = http_request_ns.class_( 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_USERAGENT = "useragent"
CONF_BODY = "body"
CONF_JSON = "json"
CONF_VERIFY_SSL = "verify_ssl" CONF_VERIFY_SSL = "verify_ssl"
CONF_ON_RESPONSE = "on_response"
CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_FOLLOW_REDIRECTS = "follow_redirects"
CONF_REDIRECT_LIMIT = "redirect_limit" 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): def validate_url(value):
value = cv.string(value) value = cv.url(value)
try: if value.startswith("http://") or value.startswith("https://"):
parsed = list(urlparse.urlparse(value)) return value
except Exception as err: raise cv.Invalid("URL must start with 'http://' or 'https://'")
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)
def validate_secure_url(config): def validate_ssl_verification(config):
url_ = config[CONF_URL] 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 ( if (
config.get(CONF_VERIFY_SSL) CORE.is_esp8266
and not isinstance(url_, Lambda) and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
and url_.lower().startswith("https:") and config[CONF_VERIFY_SSL]
): ):
error_message = "ESPHome does not support certificate verification on ESP8266"
if len(error_message) > 0:
raise cv.Invalid( raise cv.Invalid(
"Currently ESPHome doesn't support SSL verification. " f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
"Set 'verify_ssl: false' to make insecure HTTPS requests."
) )
return config 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( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(HttpRequestComponent), cv.GenerateID(): _declare_request_class,
cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string, cv.Optional(
CONF_USERAGENT, f"ESPHome/{__version__} (https://esphome.io)"
): cv.string,
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean, cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_, cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
cv.Optional( cv.Optional(
@ -81,12 +104,21 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All( cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
cv.only_on_esp8266, cv.boolean 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), ).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version( cv.require_framework_version(
esp8266_arduino=cv.Version(2, 5, 1), esp8266_arduino=cv.Version(2, 5, 1),
esp32_arduino=cv.Version(0, 0, 0), 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]: if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") 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.is_esp32:
cg.add_library("WiFiClientSecure", None) if CORE.using_esp_idf:
cg.add_library("HTTPClient", None) 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: if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None) 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_component(var, config)
@ -116,12 +167,16 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_HEADERS): cv.All( cv.Optional(CONF_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)}) 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.Optional(CONF_ON_RESPONSE): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} {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( HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf(
CONF_URL, CONF_URL,
HTTP_REQUEST_ACTION_SCHEMA.extend( 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) template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_)) cg.add(var.set_url(template_))
cg.add(var.set_method(config[CONF_METHOD])) 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: if CONF_BODY in config:
template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string) template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string)
cg.add(var.set_body(template_)) 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]) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
cg.add(var.register_response_trigger(trigger)) cg.add(var.register_response_trigger(trigger))
await automation.build_automation( 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 return var

View file

@ -1,9 +1,8 @@
#ifdef USE_ARDUINO
#include "http_request.h" #include "http_request.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/components/network/util.h"
#include <cinttypes>
namespace esphome { namespace esphome {
namespace http_request { namespace http_request {
@ -14,131 +13,12 @@ void HttpRequestComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HTTP Request:"); ESP_LOGCONFIG(TAG, "HTTP Request:");
ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_); ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_);
ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_); 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_); 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 http_request
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,27 +1,18 @@
#pragma once #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 <list>
#include <map> #include <map>
#include <memory> #include <memory>
#include <utility> #include <utility>
#include <vector> #include <vector>
#ifdef USE_ESP32 #include "esphome/components/json/json_util.h"
#include <HTTPClient.h> #include "esphome/core/application.h"
#endif #include "esphome/core/automation.h"
#ifdef USE_ESP8266 #include "esphome/core/component.h"
#include <ESP8266HTTPClient.h> #include "esphome/core/defines.h"
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS #include "esphome/core/helpers.h"
#include <WiFiClientSecure.h> #include "esphome/core/log.h"
#endif
#endif
namespace esphome { namespace esphome {
namespace http_request { namespace http_request {
@ -31,9 +22,32 @@ struct Header {
const char *value; const char *value;
}; };
class HttpRequestResponseTrigger : public Trigger<int32_t, uint32_t> { class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
public: 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 { class HttpRequestComponent : public Component {
@ -41,37 +55,33 @@ class HttpRequestComponent : public Component {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } 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_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } 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_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } 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); } std::shared_ptr<HttpContainer> get(std::string url) { return this->start(std::move(url), "GET", "", {}); }
void send(const std::vector<HttpRequestResponseTrigger *> &response_triggers); std::shared_ptr<HttpContainer> get(std::string url, std::list<Header> headers) {
void close(); return this->start(std::move(url), "GET", "", std::move(headers));
const char *get_string(); }
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: protected:
HTTPClient client_{};
std::string url_;
std::string last_url_;
const char *method_;
const char *useragent_{nullptr}; const char *useragent_{nullptr};
bool secure_;
bool follow_redirects_; bool follow_redirects_;
uint16_t redirect_limit_; uint16_t redirect_limit_;
uint16_t timeout_{5000}; uint16_t timeout_{5000};
std::string body_; uint32_t watchdog_timeout_{0};
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
}; };
template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { 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(std::string, url)
TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(const char *, method)
TEMPLATABLE_VALUE(std::string, body) 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}); } 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 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 { void play(Ts... x) override {
this->parent_->set_url(this->url_.value(x...)); std::string body;
this->parent_->set_method(this->method_.value(x...));
if (this->body_.has_value()) { if (this->body_.has_value()) {
this->parent_->set_body(this->body_.value(x...)); body = this->body_.value(x...);
} }
if (!this->json_.empty()) { if (!this->json_.empty()) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1); 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) { if (this->json_func_ != nullptr) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1); 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; std::list<Header> headers;
for (const auto &item : this->headers_) { for (const auto &item : this->headers_) {
@ -111,10 +125,37 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
header.value = val.value(x...); header.value = val.value(x...);
headers.push_back(header); headers.push_back(header);
} }
this->parent_->set_headers(headers);
this->parent_->send(this->response_triggers_); auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers);
this->parent_->close();
this->parent_->set_body(""); 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: protected:
@ -130,9 +171,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{}; std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr}; std::function<void(Ts..., JsonObject)> json_func_{nullptr};
std::vector<HttpRequestResponseTrigger *> response_triggers_; std::vector<HttpRequestResponseTrigger *> response_triggers_;
size_t max_response_buffer_size_{SIZE_MAX};
}; };
} // namespace http_request } // namespace http_request
} // namespace esphome } // 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 import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.const import ( from esphome.const import (
CONF_ESP8266_DISABLE_SSL_SUPPORT,
CONF_ID, CONF_ID,
CONF_PASSWORD, CONF_PASSWORD,
CONF_TIMEOUT,
CONF_URL, CONF_URL,
CONF_USERNAME, CONF_USERNAME,
) )
from esphome.components import esp32
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
from esphome.core import CORE, coroutine_with_priority from esphome.core import coroutine_with_priority
from .. import http_request_ns from .. import CONF_HTTP_REQUEST_ID, http_request_ns, HttpRequestComponent
CODEOWNERS = ["@oarcher"] CODEOWNERS = ["@oarcher"]
AUTO_LOAD = ["md5"] AUTO_LOAD = ["md5"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network", "http_request"]
CONF_MD5 = "md5" CONF_MD5 = "md5"
CONF_MD5_URL = "md5_url" CONF_MD5_URL = "md5_url"
CONF_VERIFY_SSL = "verify_ssl"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
OtaHttpRequestComponent = http_request_ns.class_( OtaHttpRequestComponent = http_request_ns.class_(
"OtaHttpRequestComponent", OTAComponent "OtaHttpRequestComponent", OTAComponent
) )
OtaHttpRequestComponentArduino = http_request_ns.class_(
"OtaHttpRequestComponentArduino", OtaHttpRequestComponent
)
OtaHttpRequestComponentIDF = http_request_ns.class_(
"OtaHttpRequestComponentIDF", OtaHttpRequestComponent
)
OtaHttpRequestComponentFlashAction = http_request_ns.class_( OtaHttpRequestComponentFlashAction = http_request_ns.class_(
"OtaHttpRequestComponentFlashAction", automation.Action "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( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): _declare_request_class, cv.GenerateID(): cv.declare_id(OtaHttpRequestComponent),
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All( cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
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,
),
} }
) )
.extend(BASE_OTA_SCHEMA) .extend(BASE_OTA_SCHEMA)
@ -98,7 +41,6 @@ CONFIG_SCHEMA = cv.All(
esp_idf=cv.Version(0, 0, 0), esp_idf=cv.Version(0, 0, 0),
rp2040_arduino=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): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await ota_to_code(var, config) 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_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( 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.GenerateID(): cv.use_id(OtaHttpRequestComponent),
cv.Optional(CONF_MD5_URL): cv.templatable(cv.url), 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_PASSWORD): cv.templatable(cv.string),
cv.Optional(CONF_USERNAME): cv.templatable(cv.string), cv.Optional(CONF_USERNAME): cv.templatable(cv.string),
cv.Required(CONF_URL): cv.templatable(cv.url), cv.Required(CONF_URL): cv.templatable(cv.url),
@ -159,7 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
@automation.register_action( @automation.register_action(
"ota_http_request.flash", "ota.http_request.flash",
OtaHttpRequestComponentFlashAction, OtaHttpRequestComponentFlashAction,
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
) )

View file

@ -1,45 +1,29 @@
#include "ota_http_request.h" #include "ota_http_request.h"
#include "watchdog.h" #include "../watchdog.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/components/md5/md5.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_esp32.h"
#include "esphome/components/ota/ota_backend_arduino_esp8266.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_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h" #include "esphome/components/ota/ota_backend_esp_idf.h"
#include "esphome/components/ota/ota_backend.h"
namespace esphome { namespace esphome {
namespace http_request { namespace http_request {
static const char *const TAG = "http_request.ota";
void OtaHttpRequestComponent::setup() { void OtaHttpRequestComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK #ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this); ota::register_ota_platform(this);
#endif #endif
} }
void OtaHttpRequestComponent::dump_config() { void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
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::set_md5_url(const std::string &url) { void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
if (!this->validate_url_(url)) { if (!this->validate_url_(url)) {
@ -58,20 +42,6 @@ void OtaHttpRequestComponent::set_url(const std::string &url) {
this->url_ = 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() { void OtaHttpRequestComponent::flash() {
if (this->url_.empty()) { if (this->url_.empty()) {
ESP_LOGE(TAG, "URL not set; cannot start update"); 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_) { if (this->update_started_) {
ESP_LOGV(TAG, "Aborting OTA backend"); ESP_LOGV(TAG, "Aborting OTA backend");
backend->abort(); backend->abort();
} }
ESP_LOGV(TAG, "Aborting HTTP connection"); ESP_LOGV(TAG, "Aborting HTTP connection");
this->http_end(); container->end();
}; };
uint8_t OtaHttpRequestComponent::do_ota_() { 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 last_progress = 0;
uint32_t update_start_time = millis(); uint32_t update_start_time = millis();
md5::MD5Digest md5_receive; 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_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str()); ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
this->http_init(url_with_auth);
if (!this->check_status()) { auto container = this->parent_->get(url_with_auth);
this->http_end();
if (container == nullptr) {
return OTA_CONNECTION_ERROR; return OTA_CONNECTION_ERROR;
} }
@ -144,18 +116,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
ESP_LOGV(TAG, "OTA backend begin"); ESP_LOGV(TAG, "OTA backend begin");
auto backend = ota::make_ota_backend(); 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) { if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "backend->begin error: %d", error_code); ESP_LOGW(TAG, "backend->begin error: %d", error_code);
this->cleanup_(std::move(backend)); this->cleanup_(std::move(backend), container);
return error_code; return error_code;
} }
this->bytes_read_ = 0; while (container->get_bytes_read() < container->content_length) {
while (this->bytes_read_ < this->body_length_) {
// read a maximum of chunk_size bytes into buf. (real read size returned) // read a maximum of chunk_size bytes into buf. (real read size returned)
int bufsize = this->http_read(buf, this->http_recv_buffer_); int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", this->bytes_read_, this->body_length_, bufsize); 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 // feed watchdog and give other tasks a chance to run
App.feed_wdt(); App.feed_wdt();
@ -163,9 +135,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
if (bufsize < 0) { if (bufsize < 0) {
ESP_LOGE(TAG, "Stream closed"); ESP_LOGE(TAG, "Stream closed");
this->cleanup_(std::move(backend)); this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR; 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 // add read bytes to MD5
md5_receive.add(buf, bufsize); md5_receive.add(buf, bufsize);
@ -176,16 +148,16 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// error code explanation available at // error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h // 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, 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_); container->get_bytes_read() - bufsize, container->content_length);
this->cleanup_(std::move(backend)); this->cleanup_(std::move(backend), container);
return error_code; return error_code;
} }
} }
uint32_t now = millis(); 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; 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); ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK #ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); 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(); this->md5_computed_ = md5_receive_str.get();
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { 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()); 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; return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else { } else {
backend->set_update_md5(md5_receive_str.get()); backend->set_update_md5(md5_receive_str.get());
} }
this->http_end(); container->end();
// feed watchdog and give other tasks a chance to run // feed watchdog and give other tasks a chance to run
App.feed_wdt(); App.feed_wdt();
@ -217,7 +189,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
error_code = backend->end(); error_code = backend->end();
if (error_code != ota::OTA_RESPONSE_OK) { if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); 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; 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_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str()); ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
this->http_init(url_with_auth); auto container = this->parent_->get(url_with_auth);
if (!this->check_status()) { if (container == nullptr) {
this->http_end(); ESP_LOGE(TAG, "Failed to connect to MD5 URL");
return false; return false;
} }
int length = this->body_length_; size_t length = container->content_length;
if (length < 0) { if (length == 0) {
this->http_end(); container->end();
return false; return false;
} }
if (length < MD5_SIZE) { if (length < MD5_SIZE) {
ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
this->body_length_); container->end();
this->http_end();
return false; return false;
} }
this->bytes_read_ = 0;
this->md5_expected_.resize(MD5_SIZE); this->md5_expected_.resize(MD5_SIZE);
auto read_len = this->http_read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); int read_len = 0;
this->http_end(); 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; return read_len == MD5_SIZE;
} }

View file

@ -1,17 +1,19 @@
#pragma once #pragma once
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/components/ota/ota_backend.h" #include "esphome/core/helpers.h"
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
#include "../http_request.h"
namespace esphome { namespace esphome {
namespace http_request { namespace http_request {
static const char *const TAG = "http_request.ota";
static const uint8_t MD5_SIZE = 32; static const uint8_t MD5_SIZE = 32;
enum OtaHttpRequestError : uint8_t { enum OtaHttpRequestError : uint8_t {
@ -20,7 +22,7 @@ enum OtaHttpRequestError : uint8_t {
OTA_CONNECTION_ERROR = 0x12, OTA_CONNECTION_ERROR = 0x12,
}; };
class OtaHttpRequestComponent : public ota::OTAComponent { class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
public: public:
void setup() override; void setup() override;
void dump_config() 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_url(const std::string &md5_url);
void set_md5(const std::string &md5) { this->md5_expected_ = md5; } void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
void set_password(const std::string &password) { this->password_ = password; } 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_url(const std::string &url);
void set_username(const std::string &username) { this->username_ = username; } void set_username(const std::string &username) { this->username_ = username; }
std::string md5_computed() { return this->md5_computed_; } std::string md5_computed() { return this->md5_computed_; }
std::string md5_expected() { return this->md5_expected_; } std::string md5_expected() { return this->md5_expected_; }
bool check_status();
void flash(); 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: 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_(); uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url); std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_(); bool http_get_md5_();
bool secure_() { return this->url_.find("https:") != std::string::npos; };
bool validate_url_(const std::string &url); bool validate_url_(const std::string &url);
std::string md5_computed_{}; std::string md5_computed_{};
@ -58,14 +52,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
std::string password_{}; std::string password_{};
std::string username_{}; std::string username_{};
std::string url_{}; std::string url_{};
size_t body_length_ = 0;
size_t bytes_read_ = 0;
int status_ = -1; int status_ = -1;
uint64_t timeout_ = 0;
bool update_started_ = false; bool update_started_ = false;
const uint16_t http_recv_buffer_ = 256; // the firmware GET chunk size static 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
}; };
} // namespace http_request } // 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" #include "watchdog.h"
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -20,14 +18,22 @@ namespace esphome {
namespace http_request { namespace http_request {
namespace watchdog { 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->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) { void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms); ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms);
@ -68,4 +74,3 @@ uint32_t WatchdogManager::get_timeout_() {
} // namespace watchdog } // namespace watchdog
} // namespace http_request } // namespace http_request
} // namespace esphome } // namespace esphome
#endif

View file

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

View file

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

View file

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

View file

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

View file

@ -60,7 +60,6 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
protected: protected:
void start_(); void start_();
// void stop_();
void watch_(); void watch_();
static void player_task(void *params); static void player_task(void *params);
@ -70,6 +69,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
QueueHandle_t event_queue_; QueueHandle_t event_queue_;
uint8_t dout_pin_{0}; uint8_t dout_pin_{0};
bool task_created_{false};
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};

View file

@ -69,6 +69,7 @@ MODELS = {
"ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay), "ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay),
"ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay), "ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay),
"ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay), "ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay),
"ST7735": ili9xxx_ns.class_("ILI9XXXST7735", ILI9XXXDisplay),
"ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay), "ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay),
"ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay),
"S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay),
@ -134,6 +135,7 @@ def _validate(config):
"ILI9341", "ILI9341",
"ILI9342", "ILI9342",
"ST7789V", "ST7789V",
"ST7735",
]: ]:
raise cv.Invalid("Selected model can't run on ESP8266.") 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_PWCTR3 = 0xC2;
static const uint8_t ILI9XXX_PWCTR4 = 0xC3; static const uint8_t ILI9XXX_PWCTR4 = 0xC3;
static const uint8_t ILI9XXX_PWCTR5 = 0xC4; 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_VMCTR1 = 0xC5;
static const uint8_t ILI9XXX_IFCTR = 0xC6; static const uint8_t ILI9XXX_IFCTR = 0xC6;
static const uint8_t ILI9XXX_VMCTR2 = 0xC7; 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_CSCON = 0xF0;
static const uint8_t ILI9XXX_ADJCTL3 = 0xF7; 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 ili9xxx
} // namespace esphome } // namespace esphome

View file

@ -34,8 +34,8 @@ void ILI9XXXDisplay::setup() {
ESP_LOGD(TAG, "Setting up ILI9xxx"); ESP_LOGD(TAG, "Setting up ILI9xxx");
this->setup_pins_(); this->setup_pins_();
this->init_lcd_(this->init_sequence_); this->init_lcd(this->init_sequence_);
this->init_lcd_(this->extra_init_sequence_.data()); this->init_lcd(this->extra_init_sequence_.data());
switch (this->pixel_mode_) { switch (this->pixel_mode_) {
case PIXEL_MODE_16: case PIXEL_MODE_16:
if (this->is_18bitdisplay_) { 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) if (addr == nullptr)
return; return;
uint8_t cmd, x, num_args; uint8_t cmd, x, num_args;

View file

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

View file

@ -370,6 +370,57 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = {
0x00 // End of list 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 // clang-format on
} // namespace ili9xxx } // namespace ili9xxx
} // namespace esphome } // namespace esphome

View file

@ -9,8 +9,6 @@ import re
import requests import requests
from magic import Magic from magic import Magic
from PIL import Image
from esphome import core from esphome import core
from esphome.components import font from esphome.components import font
from esphome import external_files 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" 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] url = value[CONF_URL]
h = hashlib.new("sha256") h = hashlib.new("sha256")
h.update(url.encode()) h.update(url.encode())
@ -117,7 +115,7 @@ def download_mdi(value):
def download_image(value): def download_image(value):
url = value[CONF_URL] url = value[CONF_URL]
path = _compute_local_image_path(value) path = compute_local_image_path(value)
download_content(url, path) 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]): 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 # This import is only needed in case of SVG images; adding it
# to the top would force configurations not using SVG to also have it # to the top would force configurations not using SVG to also have it
# installed for no reason. # installed for no reason.
@ -286,6 +287,9 @@ def load_svg_image(file: bytes, resize: tuple[int, int]):
async def to_code(config): 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] conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL: 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() path = _compute_local_icon_path(conf_file).as_posix()
elif conf_file[CONF_SOURCE] == SOURCE_WEB: elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = _compute_local_image_path(conf_file).as_posix() path = compute_local_image_path(conf_file).as_posix()
try: try:
with open(path, "rb") as f: 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, // Here we are allocating 1.5 times the data size,
// with the heap size minus 2kb to be safe if less than that // with the heap size minus 2kb to be safe if less than that
// as we can not have a true dynamic sized document. // 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) #elif defined(USE_LIBRETINY)
const size_t free_heap = lt_heap_get_free(); const size_t free_heap = lt_heap_get_free();
#endif #endif
bool pass = false;
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
do { while (true) {
DynamicJsonDocument json_document(request_size); DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) { if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
free_heap); free_heap);
return; return false;
} }
DeserializationError err = deserializeJson(json_document, data); DeserializationError err = deserializeJson(json_document, data);
json_document.shrinkToFit(); 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>(); JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) { if (err == DeserializationError::Ok) {
pass = true; return f(root);
f(root);
} else if (err == DeserializationError::NoMemory) { } else if (err == DeserializationError::NoMemory) {
if (request_size * 2 >= free_heap) { if (request_size * 2 >= free_heap) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return; return false;
} }
ESP_LOGV(TAG, "Increasing memory allocation."); ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2; request_size *= 2;
continue; continue;
} else { } else {
ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); ESP_LOGE(TAG, "JSON parse error: %s", err.c_str());
return; return false;
} }
} while (!pass); };
return false;
} }
} // namespace json } // namespace json

View file

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

View file

@ -312,6 +312,7 @@ async def to_code(config):
esp32.add_idf_component( esp32.add_idf_component(
name="esp-tflite-micro", name="esp-tflite-micro",
repo="https://github.com/espressif/esp-tflite-micro", repo="https://github.com/espressif/esp-tflite-micro",
ref="v1.3.1",
) )
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") 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) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent) MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent)
MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent) MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent)
MQTTUpdateComponent = mqtt_ns.class_("MQTTUpdateComponent", MQTTComponent)
MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent) MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent)
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") 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) { 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) { 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{ MQTTSubscription subscription{
.topic = topic, .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_DISARM = "pl_disarm";
constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd"; 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_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_LOCATE = "pl_loc";
constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock"; constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock";
constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd"; 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_DISARM = "payload_disarm";
constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed"; 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_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_LOCATE = "payload_locate";
constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock"; constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock";
constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed"; constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed";

View file

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

View file

@ -3,7 +3,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_MQTT #ifdef USE_MQTT
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h" #include "esphome/components/datetime/datetime_entity.h"
#include "mqtt_component.h" #include "mqtt_component.h"
@ -17,7 +17,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
* *
* @param time The time entity. * @param time The time entity.
*/ */
explicit MQTTDateTimeComponent(datetime::DateTimeEntity *time); explicit MQTTDateTimeComponent(datetime::DateTimeEntity *datetime);
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
@ -41,5 +41,5 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent {
} // namespace mqtt } // namespace mqtt
} // namespace esphome } // namespace esphome
#endif // USE_DATETIME_DATE #endif // USE_DATETIME_DATETIME
#endif // USE_MQTT #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): def get_download_types(storage_json):
return [ return [
{ {
"title": "UF2 format", "title": "UF2 factory format",
"description": "For copying to RP2040 over USB.", "description": "For copying to RP2040 over USB.",
"file": "firmware.uf2", "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_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040") cg.add_define("ESPHOME_VARIANT", "RP2040")
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino") cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
@ -225,4 +233,10 @@ def generate_pio_files() -> bool:
# Called by writer.py # Called by writer.py
def copy_files() -> bool: 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() 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"] CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"]
CONF_BOOT_IS_GOOD_AFTER = "boot_is_good_after"
CONF_ON_SAFE_MODE = "on_safe_mode" CONF_ON_SAFE_MODE = "on_safe_mode"
safe_mode_ns = cg.esphome_ns.namespace("safe_mode") safe_mode_ns = cg.esphome_ns.namespace("safe_mode")
@ -34,6 +35,9 @@ CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(SafeModeComponent), 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_DISABLED, default=False): cv.boolean,
cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
cv.Optional( cv.Optional(
@ -63,7 +67,9 @@ async def to_code(config):
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
condition = var.should_enter_safe_mode( 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")) cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_SAFE_MODE] = {} CORE.data[CONF_SAFE_MODE] = {}

View file

@ -16,6 +16,8 @@ static const char *const TAG = "safe_mode";
void SafeModeComponent::dump_config() { void SafeModeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Safe Mode:"); 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, " Invoke after %u boot attempts", this->safe_mode_num_attempts_);
ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds", ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds",
this->safe_mode_enable_time_ / 1000); // because milliseconds 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; } float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void SafeModeComponent::loop() { 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 // successful boot, reset counter
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc(); this->clean_rtc();
@ -60,9 +62,11 @@ bool SafeModeComponent::get_safe_mode_pending() {
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; 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_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time; 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->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false); this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
this->safe_mode_rtc_value_ = this->read_rtc_(); 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 /// SafeModeComponent provides a safe way to recover from repeated boot failures
class SafeModeComponent : public Component { class SafeModeComponent : public Component {
public: 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 /// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending); void set_safe_mode_pending(const bool &pending);
@ -33,11 +33,12 @@ class SafeModeComponent : public Component {
void write_rtc_(uint32_t val); void write_rtc_(uint32_t val);
uint32_t read_rtc_(); uint32_t read_rtc_();
bool boot_successful_{false}; ///< set to true after boot is considered successful 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_enable_time_{60000}; ///< The time safe mode should remain active for
uint32_t safe_mode_rtc_value_; uint32_t safe_mode_rtc_value_{0};
uint8_t safe_mode_num_attempts_; uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled
uint8_t safe_mode_num_attempts_{0};
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
CallbackManager<void()> safe_mode_callback_{}; 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