mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 15:41:52 +00:00
Compare commits
37 Commits
jesserockz
...
2024.6.0b2
Author | SHA1 | Date | |
---|---|---|---|
|
4e8a7986cd | ||
|
3db71b98ae | ||
|
73cb3ec852 | ||
|
91e72fe121 | ||
|
be486e0ca6 | ||
|
fdefc825bb | ||
|
8a83670f54 | ||
|
f9f98fa6c6 | ||
|
f25c296303 | ||
|
bc408ad08c | ||
|
e2c1af199c | ||
|
7c843437a7 | ||
|
4bf7c97088 | ||
|
7b9fb57bb2 | ||
|
699d00e218 | ||
|
e2784d077d | ||
|
13fabf1cd8 | ||
|
7b60543afd | ||
|
562700bd2c | ||
|
a64106e48c | ||
|
c723fd1f80 | ||
|
3a97244b83 | ||
|
1f8449ec0e | ||
|
3cd2fb0843 | ||
|
7dc07c5632 | ||
|
95e45dc12c | ||
|
51a8a7e875 | ||
|
dceab6ce29 | ||
|
6de79d6cfb | ||
|
7b45498de6 | ||
|
618102fe8c | ||
|
38b7bed2fa | ||
|
d77ea46157 | ||
|
8718e15a6a | ||
|
861a23d039 | ||
|
276eea2b69 | ||
|
ccab57fc58 |
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -46,7 +46,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
uses: docker/build-push-action@v5.4.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
@@ -69,7 +69,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
uses: docker/build-push-action@v5.4.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -454,8 +454,8 @@ jobs:
|
||||
matrix:
|
||||
file: ${{ fromJson(needs.list-components.outputs.components) }}
|
||||
steps:
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install libsodium-dev
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install libsodium-dev libsdl2-dev
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
@@ -508,8 +508,8 @@ jobs:
|
||||
- name: List components
|
||||
run: echo ${{ matrix.components }}
|
||||
|
||||
- name: Install libsodium
|
||||
run: sudo apt-get install libsodium-dev
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install libsodium-dev libsdl2-dev
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
@@ -94,6 +94,7 @@ esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/dac7678/* @NickB1
|
||||
esphome/components/daikin_arc/* @MagicBear
|
||||
esphome/components/daikin_brc/* @hagak
|
||||
esphome/components/dallas_temp/* @ssieb
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/datetime/* @jesserockz @rfdarter
|
||||
@@ -144,6 +145,7 @@ esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/globals/* @esphome/core
|
||||
esphome/components/gp8403/* @jesserockz
|
||||
esphome/components/gpio/* @esphome/core
|
||||
esphome/components/gpio/one_wire/* @ssieb
|
||||
esphome/components/gps/* @coogle
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/graphical_display_menu/* @MrMDavidson
|
||||
@@ -172,6 +174,7 @@ esphome/components/host/time/* @clydebarrow
|
||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
|
||||
esphome/components/hte501/* @Stock-M
|
||||
esphome/components/http_request/ota/* @oarcher
|
||||
esphome/components/http_request/update/* @jesserockz
|
||||
esphome/components/htu31d/* @betterengineering
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/hyt271/* @Philippe12
|
||||
@@ -269,6 +272,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
|
||||
esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
@@ -316,6 +320,7 @@ esphome/components/rtttl/* @glmnet
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
esphome/components/sdl/* @clydebarrow
|
||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||
esphome/components/sdp3x/* @Azimath
|
||||
esphome/components/seeed_mr24hpc1/* @limengdu
|
||||
@@ -410,6 +415,7 @@ esphome/components/uart/button/* @ssieb
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
esphome/components/update/* @jesserockz
|
||||
esphome/components/uponor_smatrix/* @kroimon
|
||||
esphome/components/valve/* @esphome/core
|
||||
esphome/components/vbus/* @ssieb
|
||||
|
@@ -81,7 +81,8 @@ RUN \
|
||||
fi; \
|
||||
pip3 install \
|
||||
--break-system-packages --no-cache-dir \
|
||||
platformio==6.1.13 \
|
||||
# Keep platformio version in sync with requirements.txt
|
||||
platformio==6.1.15 \
|
||||
# Change some platformio settings
|
||||
&& platformio settings set enable_telemetry No \
|
||||
&& platformio settings set check_platformio_interval 1000000 \
|
||||
@@ -101,7 +102,7 @@ RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "a
|
||||
&& /platformio_install_deps.py /platformio.ini --libraries
|
||||
|
||||
# Avoid unsafe git error when container user and file config volume permissions don't match
|
||||
RUN git config --system --add safe.directory '/config/*'
|
||||
RUN git config --system --add safe.directory '*'
|
||||
|
||||
|
||||
# ======================= docker-type image =======================
|
||||
|
@@ -488,6 +488,15 @@ def command_run(args, config):
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
if CORE.is_host:
|
||||
from esphome.platformio_api import get_idedata
|
||||
|
||||
idedata = get_idedata(config)
|
||||
if idedata is None:
|
||||
return 1
|
||||
program_path = idedata.raw["prog_path"]
|
||||
return run_external_process(program_path)
|
||||
|
||||
port = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
|
@@ -58,6 +58,7 @@ from esphome.cpp_types import ( # noqa
|
||||
bool_,
|
||||
int_,
|
||||
std_ns,
|
||||
std_shared_ptr,
|
||||
std_string,
|
||||
std_vector,
|
||||
uint8,
|
||||
|
@@ -3,7 +3,13 @@ import logging
|
||||
from esphome import automation, core
|
||||
from esphome.components import font
|
||||
import esphome.components.image as espImage
|
||||
from esphome.components.image import CONF_USE_TRANSPARENCY
|
||||
from esphome.components.image import (
|
||||
CONF_USE_TRANSPARENCY,
|
||||
LOCAL_SCHEMA,
|
||||
WEB_SCHEMA,
|
||||
SOURCE_WEB,
|
||||
SOURCE_LOCAL,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
@@ -13,6 +19,9 @@ from esphome.const import (
|
||||
CONF_REPEAT,
|
||||
CONF_RESIZE,
|
||||
CONF_TYPE,
|
||||
CONF_SOURCE,
|
||||
CONF_PATH,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
|
||||
@@ -43,6 +52,40 @@ SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
SOURCE_LOCAL: LOCAL_SCHEMA,
|
||||
SOURCE_WEB: WEB_SCHEMA,
|
||||
},
|
||||
key=CONF_SOURCE,
|
||||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_cross_dependencies(config):
|
||||
"""
|
||||
@@ -67,7 +110,7 @@ ANIMATION_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Required(CONF_FILE): cv.file_,
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
||||
espImage.IMAGE_TYPE, upper=True
|
||||
@@ -124,7 +167,11 @@ async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
async def to_code(config):
|
||||
from PIL import Image
|
||||
|
||||
path = CORE.relative_config_path(config[CONF_FILE])
|
||||
conf_file = config[CONF_FILE]
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||
path = espImage.compute_local_image_path(conf_file).as_posix()
|
||||
try:
|
||||
image = Image.open(path)
|
||||
except Exception as e:
|
||||
|
@@ -48,6 +48,7 @@ service APIConnection {
|
||||
rpc date_command (DateCommandRequest) returns (void) {}
|
||||
rpc time_command (TimeCommandRequest) returns (void) {}
|
||||
rpc datetime_command (DateTimeCommandRequest) returns (void) {}
|
||||
rpc update_command (UpdateCommandRequest) returns (void) {}
|
||||
|
||||
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
||||
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
|
||||
@@ -1837,3 +1838,46 @@ message DateTimeCommandRequest {
|
||||
fixed32 key = 1;
|
||||
fixed32 epoch_seconds = 2;
|
||||
}
|
||||
|
||||
// ==================== UPDATE ====================
|
||||
message ListEntitiesUpdateResponse {
|
||||
option (id) = 116;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_UPDATE";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string unique_id = 4;
|
||||
|
||||
string icon = 5;
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
}
|
||||
message UpdateStateResponse {
|
||||
option (id) = 117;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_UPDATE";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
bool missing_state = 2;
|
||||
bool in_progress = 3;
|
||||
bool has_progress = 4;
|
||||
float progress = 5;
|
||||
string current_version = 6;
|
||||
string latest_version = 7;
|
||||
string title = 8;
|
||||
string release_summary = 9;
|
||||
string release_url = 10;
|
||||
}
|
||||
message UpdateCommandRequest {
|
||||
option (id) = 118;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_UPDATE";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
bool install = 2;
|
||||
}
|
||||
|
@@ -1287,6 +1287,51 @@ bool APIConnection::send_event_info(event::Event *event) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool APIConnection::send_update_state(update::UpdateEntity *update) {
|
||||
if (!this->state_subscription_)
|
||||
return false;
|
||||
|
||||
UpdateStateResponse resp{};
|
||||
resp.key = update->get_object_id_hash();
|
||||
resp.missing_state = !update->has_state();
|
||||
if (update->has_state()) {
|
||||
resp.in_progress = update->state == update::UpdateState::UPDATE_STATE_INSTALLING;
|
||||
if (update->update_info.has_progress) {
|
||||
resp.has_progress = true;
|
||||
resp.progress = update->update_info.progress;
|
||||
}
|
||||
resp.current_version = update->update_info.current_version;
|
||||
resp.latest_version = update->update_info.latest_version;
|
||||
resp.title = update->update_info.title;
|
||||
resp.release_summary = update->update_info.summary;
|
||||
resp.release_url = update->update_info.release_url;
|
||||
}
|
||||
|
||||
return this->send_update_state_response(resp);
|
||||
}
|
||||
bool APIConnection::send_update_info(update::UpdateEntity *update) {
|
||||
ListEntitiesUpdateResponse msg;
|
||||
msg.key = update->get_object_id_hash();
|
||||
msg.object_id = update->get_object_id();
|
||||
if (update->has_own_name())
|
||||
msg.name = update->get_name();
|
||||
msg.unique_id = get_default_unique_id("update", update);
|
||||
msg.icon = update->get_icon();
|
||||
msg.disabled_by_default = update->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(update->get_entity_category());
|
||||
msg.device_class = update->get_device_class();
|
||||
return this->send_list_entities_update_response(msg);
|
||||
}
|
||||
void APIConnection::update_command(const UpdateCommandRequest &msg) {
|
||||
update::UpdateEntity *update = App.get_update_by_key(msg.key);
|
||||
if (update == nullptr)
|
||||
return;
|
||||
|
||||
update->perform();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
|
||||
if (this->log_subscription_ < level)
|
||||
return false;
|
||||
|
@@ -164,6 +164,12 @@ class APIConnection : public APIServerConnection {
|
||||
bool send_event_info(event::Event *event);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state(update::UpdateEntity *update);
|
||||
bool send_update_info(update::UpdateEntity *update);
|
||||
void update_command(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
void on_disconnect_response(const DisconnectResponse &value) override;
|
||||
void on_ping_response(const PingResponse &value) override {
|
||||
// we initiated ping
|
||||
|
@@ -8376,6 +8376,262 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 6: {
|
||||
this->disabled_by_default = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 7: {
|
||||
this->entity_category = value.as_enum<enums::EntityCategory>();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesUpdateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->object_id = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->name = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->unique_id = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
this->icon = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 8: {
|
||||
this->device_class = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ListEntitiesUpdateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id);
|
||||
buffer.encode_fixed32(2, this->key);
|
||||
buffer.encode_string(3, this->name);
|
||||
buffer.encode_string(4, this->unique_id);
|
||||
buffer.encode_string(5, this->icon);
|
||||
buffer.encode_bool(6, this->disabled_by_default);
|
||||
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
|
||||
buffer.encode_string(8, this->device_class);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("ListEntitiesUpdateResponse {\n");
|
||||
out.append(" object_id: ");
|
||||
out.append("'").append(this->object_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" unique_id: ");
|
||||
out.append("'").append(this->unique_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" icon: ");
|
||||
out.append("'").append(this->icon).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" disabled_by_default: ");
|
||||
out.append(YESNO(this->disabled_by_default));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" entity_category: ");
|
||||
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" device_class: ");
|
||||
out.append("'").append(this->device_class).append("'");
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->missing_state = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->in_progress = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 4: {
|
||||
this->has_progress = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool UpdateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 6: {
|
||||
this->current_version = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 7: {
|
||||
this->latest_version = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 8: {
|
||||
this->title = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 9: {
|
||||
this->release_summary = value.as_string();
|
||||
return true;
|
||||
}
|
||||
case 10: {
|
||||
this->release_url = value.as_string();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool UpdateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
this->progress = value.as_float();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_bool(2, this->missing_state);
|
||||
buffer.encode_bool(3, this->in_progress);
|
||||
buffer.encode_bool(4, this->has_progress);
|
||||
buffer.encode_float(5, this->progress);
|
||||
buffer.encode_string(6, this->current_version);
|
||||
buffer.encode_string(7, this->latest_version);
|
||||
buffer.encode_string(8, this->title);
|
||||
buffer.encode_string(9, this->release_summary);
|
||||
buffer.encode_string(10, this->release_url);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void UpdateStateResponse::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("UpdateStateResponse {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" missing_state: ");
|
||||
out.append(YESNO(this->missing_state));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" in_progress: ");
|
||||
out.append(YESNO(this->in_progress));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" has_progress: ");
|
||||
out.append(YESNO(this->has_progress));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" progress: ");
|
||||
sprintf(buffer, "%g", this->progress);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" current_version: ");
|
||||
out.append("'").append(this->current_version).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" latest_version: ");
|
||||
out.append("'").append(this->latest_version).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" title: ");
|
||||
out.append("'").append(this->title).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" release_summary: ");
|
||||
out.append("'").append(this->release_summary).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" release_url: ");
|
||||
out.append("'").append(this->release_url).append("'");
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->install = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->key = value.as_fixed32();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
buffer.encode_bool(2, this->install);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void UpdateCommandRequest::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("UpdateCommandRequest {\n");
|
||||
out.append(" key: ");
|
||||
sprintf(buffer, "%" PRIu32, this->key);
|
||||
out.append(buffer);
|
||||
out.append("\n");
|
||||
|
||||
out.append(" install: ");
|
||||
out.append(YESNO(this->install));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@@ -2130,6 +2130,61 @@ class DateTimeCommandRequest : public ProtoMessage {
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
};
|
||||
class ListEntitiesUpdateResponse : public ProtoMessage {
|
||||
public:
|
||||
std::string object_id{};
|
||||
uint32_t key{0};
|
||||
std::string name{};
|
||||
std::string unique_id{};
|
||||
std::string icon{};
|
||||
bool disabled_by_default{false};
|
||||
enums::EntityCategory entity_category{};
|
||||
std::string device_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class UpdateStateResponse : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
bool missing_state{false};
|
||||
bool in_progress{false};
|
||||
bool has_progress{false};
|
||||
float progress{0.0f};
|
||||
std::string current_version{};
|
||||
std::string latest_version{};
|
||||
std::string title{};
|
||||
std::string release_summary{};
|
||||
std::string release_url{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class UpdateCommandRequest : public ProtoMessage {
|
||||
public:
|
||||
uint32_t key{0};
|
||||
bool install{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@@ -611,6 +611,24 @@ bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateR
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool APIServerConnectionBase::send_list_entities_update_response(const ListEntitiesUpdateResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_list_entities_update_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<ListEntitiesUpdateResponse>(msg, 116);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool APIServerConnectionBase::send_update_state_response(const UpdateStateResponse &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "send_update_state_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
return this->send_message_<UpdateStateResponse>(msg, 117);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
#endif
|
||||
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
switch (msg_type) {
|
||||
case 1: {
|
||||
@@ -1106,6 +1124,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_voice_assistant_timer_event_response(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case 118: {
|
||||
#ifdef USE_UPDATE
|
||||
UpdateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_update_command_request(msg);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -1434,6 +1463,19 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
|
||||
this->datetime_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return;
|
||||
}
|
||||
this->update_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
|
@@ -306,6 +306,15 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
virtual void on_date_time_command_request(const DateTimeCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool send_list_entities_update_response(const ListEntitiesUpdateResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state_response(const UpdateStateResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
virtual void on_update_command_request(const UpdateCommandRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
@@ -373,6 +382,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
virtual void update_command(const UpdateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
@@ -471,6 +483,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
|
@@ -334,6 +334,13 @@ void APIServer::on_event(event::Event *obj, const std::string &event_type) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
for (auto &c : this->clients_)
|
||||
c->send_update_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@@ -102,6 +102,9 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj, const std::string &event_type) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update(update::UpdateEntity *obj) override;
|
||||
#endif
|
||||
|
||||
bool is_connected() const;
|
||||
|
||||
|
@@ -98,6 +98,9 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
|
||||
#ifdef USE_EVENT
|
||||
bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); }
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); }
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
|
@@ -75,6 +75,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *update) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
|
||||
|
@@ -77,6 +77,9 @@ bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
|
||||
return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); }
|
||||
#endif
|
||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
} // namespace api
|
||||
|
@@ -72,6 +72,9 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *update) override;
|
||||
#endif
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
@@ -6,18 +6,24 @@ namespace climate_ir_lg {
|
||||
|
||||
static const char *const TAG = "climate.climate_ir_lg";
|
||||
|
||||
const uint32_t COMMAND_ON = 0x00000;
|
||||
const uint32_t COMMAND_ON_AI = 0x03000;
|
||||
const uint32_t COMMAND_COOL = 0x08000;
|
||||
const uint32_t COMMAND_HEAT = 0x0C000;
|
||||
// Commands
|
||||
const uint32_t COMMAND_MASK = 0xFF000;
|
||||
const uint32_t COMMAND_OFF = 0xC0000;
|
||||
const uint32_t COMMAND_SWING = 0x10000;
|
||||
// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore.
|
||||
const uint32_t COMMAND_AUTO = 0x0B000;
|
||||
const uint32_t COMMAND_DRY_FAN = 0x09000;
|
||||
|
||||
const uint32_t COMMAND_MASK = 0xFF000;
|
||||
const uint32_t COMMAND_ON_COOL = 0x00000;
|
||||
const uint32_t COMMAND_ON_DRY = 0x01000;
|
||||
const uint32_t COMMAND_ON_FAN_ONLY = 0x02000;
|
||||
const uint32_t COMMAND_ON_AI = 0x03000;
|
||||
const uint32_t COMMAND_ON_HEAT = 0x04000;
|
||||
|
||||
const uint32_t COMMAND_COOL = 0x08000;
|
||||
const uint32_t COMMAND_DRY = 0x09000;
|
||||
const uint32_t COMMAND_FAN_ONLY = 0x0A000;
|
||||
const uint32_t COMMAND_AI = 0x0B000;
|
||||
const uint32_t COMMAND_HEAT = 0x0C000;
|
||||
|
||||
// Fan speed
|
||||
const uint32_t FAN_MASK = 0xF0;
|
||||
const uint32_t FAN_AUTO = 0x50;
|
||||
const uint32_t FAN_MIN = 0x00;
|
||||
@@ -35,69 +41,67 @@ void LgIrClimate::transmit_state() {
|
||||
uint32_t remote_state = 0x8800000;
|
||||
|
||||
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
|
||||
|
||||
// Set command
|
||||
if (send_swing_cmd_) {
|
||||
send_swing_cmd_ = false;
|
||||
remote_state |= COMMAND_SWING;
|
||||
} else {
|
||||
if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
|
||||
remote_state |= COMMAND_ON_AI;
|
||||
} else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) {
|
||||
remote_state |= COMMAND_ON;
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
} else {
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
remote_state |= COMMAND_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
remote_state |= COMMAND_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
remote_state |= COMMAND_AUTO;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
remote_state |= COMMAND_DRY_FAN;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
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);
|
||||
bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF);
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
remote_state |= climate_is_off ? COMMAND_ON_DRY : COMMAND_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
remote_state |= climate_is_off ? COMMAND_ON_FAN_ONLY : COMMAND_FAN_ONLY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
remote_state |= climate_is_off ? COMMAND_ON_AI : COMMAND_AI;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
remote_state |= climate_is_off ? COMMAND_ON_HEAT : COMMAND_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
remote_state |= COMMAND_OFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mode_before_ = this->mode;
|
||||
|
||||
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
|
||||
|
||||
// Set fan speed
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
remote_state |= FAN_AUTO;
|
||||
} else {
|
||||
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);
|
||||
this->publish_state();
|
||||
}
|
||||
@@ -125,37 +129,42 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
if ((remote_state & 0xFF00000) != 0x8800000)
|
||||
return false;
|
||||
|
||||
if ((remote_state & COMMAND_MASK) == COMMAND_ON) {
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
} else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
|
||||
// Get command
|
||||
if ((remote_state & COMMAND_MASK) == COMMAND_OFF) {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) {
|
||||
this->swing_mode =
|
||||
this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
|
||||
} else {
|
||||
if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
} else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) {
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
} else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
switch (remote_state & COMMAND_MASK) {
|
||||
case COMMAND_DRY:
|
||||
case COMMAND_ON_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case COMMAND_FAN_ONLY:
|
||||
case COMMAND_ON_FAN_ONLY:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
case COMMAND_AI:
|
||||
case COMMAND_ON_AI:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case COMMAND_HEAT:
|
||||
case COMMAND_ON_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case COMMAND_COOL:
|
||||
case COMMAND_ON_COOL:
|
||||
default:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT)
|
||||
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
|
||||
|
||||
// Fan Speed
|
||||
// Get fan speed
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT ||
|
||||
this->mode == climate::CLIMATE_MODE_DRY) {
|
||||
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
|
||||
this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
if ((remote_state & FAN_MASK) == FAN_AUTO) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
} else if ((remote_state & FAN_MASK) == FAN_MIN) {
|
||||
@@ -166,11 +175,17 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
}
|
||||
}
|
||||
|
||||
// Get temperature
|
||||
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
|
||||
}
|
||||
}
|
||||
this->publish_state();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LgIrClimate::transmit_(uint32_t value) {
|
||||
calc_checksum_(value);
|
||||
ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value);
|
||||
|
@@ -14,7 +14,7 @@ const uint8_t TEMP_MAX = 30; // Celsius
|
||||
class LgIrClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
LgIrClimate()
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false,
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
|
||||
|
@@ -1,25 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
|
||||
MULTI_CONF = True
|
||||
AUTO_LOAD = ["sensor"]
|
||||
|
||||
dallas_ns = cg.esphome_ns.namespace("dallas")
|
||||
DallasComponent = dallas_ns.class_("DallasComponent", cg.PollingComponent)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DallasComponent),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
CONFIG_SCHEMA = cv.invalid(
|
||||
'The "dallas" component has been replaced by the "one_wire" component.\nhttps://esphome.io/components/one_wire'
|
||||
)
|
||||
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -1,50 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_DALLAS_ID,
|
||||
CONF_INDEX,
|
||||
CONF_RESOLUTION,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
|
||||
CONFIG_SCHEMA = cv.invalid(
|
||||
'The "dallas" sensor is now "dallas_temp"\nhttps://esphome.io/components/sensor/dallas_temp'
|
||||
)
|
||||
from . import DallasComponent, dallas_ns
|
||||
|
||||
DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sensor)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
DallasTemperatureSensor,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent),
|
||||
cv.Optional(CONF_ADDRESS): cv.hex_uint64_t,
|
||||
cv.Optional(CONF_INDEX): cv.positive_int,
|
||||
cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_ADDRESS, CONF_INDEX),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_DALLAS_ID])
|
||||
var = await sensor.new_sensor(config)
|
||||
|
||||
if CONF_ADDRESS in config:
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
else:
|
||||
cg.add(var.set_index(config[CONF_INDEX]))
|
||||
|
||||
if CONF_RESOLUTION in config:
|
||||
cg.add(var.set_resolution(config[CONF_RESOLUTION]))
|
||||
|
||||
cg.add(var.set_parent(hub))
|
||||
|
||||
cg.add(hub.register_sensor(var))
|
||||
|
1
esphome/components/dallas_temp/__init__.py
Normal file
1
esphome/components/dallas_temp/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@ssieb"]
|
172
esphome/components/dallas_temp/dallas_temp.cpp
Normal file
172
esphome/components/dallas_temp/dallas_temp.cpp
Normal 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
|
32
esphome/components/dallas_temp/dallas_temp.h
Normal file
32
esphome/components/dallas_temp/dallas_temp.h
Normal 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
|
43
esphome/components/dallas_temp/sensor.py
Normal file
43
esphome/components/dallas_temp/sensor.py
Normal 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]))
|
@@ -80,6 +80,17 @@ void DateCall::validate_() {
|
||||
|
||||
void DateCall::perform() {
|
||||
this->validate_();
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
|
||||
if (this->year_.has_value()) {
|
||||
ESP_LOGD(TAG, " Year: %d", *this->year_);
|
||||
}
|
||||
if (this->month_.has_value()) {
|
||||
ESP_LOGD(TAG, " Month: %d", *this->month_);
|
||||
}
|
||||
if (this->day_.has_value()) {
|
||||
ESP_LOGD(TAG, " Day: %d", *this->day_);
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
|
||||
|
@@ -34,10 +34,12 @@ enum WakeupPinMode {
|
||||
WAKEUP_PIN_MODE_INVERT_WAKEUP,
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
struct Ext1Wakeup {
|
||||
uint64_t mask;
|
||||
esp_sleep_ext1_wakeup_mode_t wakeup_mode;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct WakeupCauseToRunDuration {
|
||||
// Run duration if woken up by timer or any other reason besides those below.
|
||||
@@ -114,7 +116,11 @@ class DeepSleepComponent : public Component {
|
||||
#ifdef USE_ESP32
|
||||
InternalGPIOPin *wakeup_pin_;
|
||||
WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
optional<Ext1Wakeup> ext1_wakeup_;
|
||||
#endif
|
||||
|
||||
optional<bool> touch_wakeup_;
|
||||
optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_;
|
||||
#endif
|
||||
|
25
esphome/components/gpio/one_wire/__init__.py
Normal file
25
esphome/components/gpio/one_wire/__init__.py
Normal 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))
|
199
esphome/components/gpio/one_wire/gpio_one_wire.cpp
Normal file
199
esphome/components/gpio/one_wire/gpio_one_wire.cpp
Normal 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 ? 54 : 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
|
41
esphome/components/gpio/one_wire/gpio_one_wire.h
Normal file
41
esphome/components/gpio/one_wire/gpio_one_wire.h
Normal 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
|
@@ -56,7 +56,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) {
|
||||
this->position = new_position;
|
||||
this->current_operation = COVER_OPERATION_IDLE;
|
||||
if (this->last_command_ == operation) {
|
||||
float dur = (now - this->start_dir_time_) / 1e3f;
|
||||
float dur = (float) (now - this->start_dir_time_) / 1e3f;
|
||||
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
|
||||
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
|
||||
}
|
||||
@@ -69,7 +69,6 @@ void HE60rCover::set_current_operation_(cover::CoverOperation operation) {
|
||||
this->current_operation = operation;
|
||||
if (operation != COVER_OPERATION_IDLE)
|
||||
this->last_recompute_time_ = millis();
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +128,7 @@ void HE60rCover::update_() {
|
||||
if (this->toggles_needed_ != 0) {
|
||||
if ((this->counter_++ & 0x3) == 0) {
|
||||
this->toggles_needed_--;
|
||||
ESP_LOGD(TAG, "Writing byte 0x30, still needed=%" PRIu32, this->toggles_needed_);
|
||||
ESP_LOGD(TAG, "Writing byte 0x30, still needed=%u", this->toggles_needed_);
|
||||
this->write_byte(TOGGLE_BYTE);
|
||||
} else {
|
||||
this->write_byte(QUERY_BYTE);
|
||||
@@ -235,31 +234,28 @@ void HE60rCover::recompute_position_() {
|
||||
return;
|
||||
|
||||
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_) {
|
||||
auto diff = now - last_recompute_time_;
|
||||
auto delta = dir * diff / action_dur;
|
||||
auto diff = (unsigned) (now - last_recompute_time_);
|
||||
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.
|
||||
this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f);
|
||||
ESP_LOGD(TAG, "Recompute %dms, dir=%f, action_dur=%f, delta=%f, pos=%f", (int) diff, dir, action_dur, delta,
|
||||
this->position);
|
||||
auto new_position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f);
|
||||
ESP_LOGD(TAG, "Recompute %ums, dir=%u, delta=%f, pos=%f", diff, this->current_operation, delta, new_position);
|
||||
this->last_recompute_time_ = now;
|
||||
this->publish_state();
|
||||
if (this->position != new_position) {
|
||||
this->position = new_position;
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -25,15 +25,14 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic
|
||||
void control(const cover::CoverCall &call) override;
|
||||
bool is_at_target_() const;
|
||||
void start_direction_(cover::CoverOperation dir);
|
||||
void update_operation_(cover::CoverOperation dir);
|
||||
void endstop_reached_(cover::CoverOperation operation);
|
||||
void recompute_position_();
|
||||
void set_current_operation_(cover::CoverOperation operation);
|
||||
void process_rx_(uint8_t data);
|
||||
|
||||
uint32_t open_duration_{0};
|
||||
uint32_t close_duration_{0};
|
||||
uint32_t toggles_needed_{0};
|
||||
unsigned open_duration_{0};
|
||||
unsigned close_duration_{0};
|
||||
unsigned toggles_needed_{0};
|
||||
cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE};
|
||||
cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE};
|
||||
uint32_t last_recompute_time_{0};
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import urllib.parse as urlparse
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.const import (
|
||||
__version__,
|
||||
CONF_ID,
|
||||
CONF_TIMEOUT,
|
||||
CONF_METHOD,
|
||||
@@ -12,67 +11,91 @@ from esphome.const import (
|
||||
CONF_ESP8266_DISABLE_SSL_SUPPORT,
|
||||
)
|
||||
from esphome.core import Lambda, CORE
|
||||
from esphome.components import esp32
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["json"]
|
||||
|
||||
http_request_ns = cg.esphome_ns.namespace("http_request")
|
||||
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
|
||||
HttpRequestArduino = http_request_ns.class_("HttpRequestArduino", HttpRequestComponent)
|
||||
HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
|
||||
|
||||
HttpContainer = http_request_ns.class_("HttpContainer")
|
||||
|
||||
HttpRequestSendAction = http_request_ns.class_(
|
||||
"HttpRequestSendAction", automation.Action
|
||||
)
|
||||
HttpRequestResponseTrigger = http_request_ns.class_(
|
||||
"HttpRequestResponseTrigger", automation.Trigger
|
||||
"HttpRequestResponseTrigger",
|
||||
automation.Trigger.template(
|
||||
cg.std_shared_ptr.template(HttpContainer), cg.std_string
|
||||
),
|
||||
)
|
||||
|
||||
CONF_HEADERS = "headers"
|
||||
CONF_HTTP_REQUEST_ID = "http_request_id"
|
||||
|
||||
CONF_USERAGENT = "useragent"
|
||||
CONF_BODY = "body"
|
||||
CONF_JSON = "json"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
CONF_ON_RESPONSE = "on_response"
|
||||
CONF_FOLLOW_REDIRECTS = "follow_redirects"
|
||||
CONF_REDIRECT_LIMIT = "redirect_limit"
|
||||
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
|
||||
|
||||
CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size"
|
||||
CONF_ON_RESPONSE = "on_response"
|
||||
CONF_HEADERS = "headers"
|
||||
CONF_BODY = "body"
|
||||
CONF_JSON = "json"
|
||||
CONF_CAPTURE_RESPONSE = "capture_response"
|
||||
|
||||
|
||||
def validate_url(value):
|
||||
value = cv.string(value)
|
||||
try:
|
||||
parsed = list(urlparse.urlparse(value))
|
||||
except Exception as err:
|
||||
raise cv.Invalid("Invalid URL") from err
|
||||
|
||||
if not parsed[0] or not parsed[1]:
|
||||
raise cv.Invalid("URL must have a URL scheme and host")
|
||||
|
||||
if parsed[0] not in ["http", "https"]:
|
||||
raise cv.Invalid("Scheme must be http or https")
|
||||
|
||||
if not parsed[2]:
|
||||
parsed[2] = "/"
|
||||
|
||||
return urlparse.urlunparse(parsed)
|
||||
value = cv.url(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return value
|
||||
raise cv.Invalid("URL must start with 'http://' or 'https://'")
|
||||
|
||||
|
||||
def validate_secure_url(config):
|
||||
url_ = config[CONF_URL]
|
||||
def validate_ssl_verification(config):
|
||||
error_message = ""
|
||||
|
||||
if CORE.is_esp32:
|
||||
if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
|
||||
error_message = "ESPHome supports certificate verification only via ESP-IDF"
|
||||
|
||||
if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
|
||||
error_message = "ESPHome does not support certificate verification on RP2040"
|
||||
|
||||
if (
|
||||
config.get(CONF_VERIFY_SSL)
|
||||
and not isinstance(url_, Lambda)
|
||||
and url_.lower().startswith("https:")
|
||||
CORE.is_esp8266
|
||||
and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
|
||||
and config[CONF_VERIFY_SSL]
|
||||
):
|
||||
error_message = "ESPHome does not support certificate verification on ESP8266"
|
||||
|
||||
if len(error_message) > 0:
|
||||
raise cv.Invalid(
|
||||
"Currently ESPHome doesn't support SSL verification. "
|
||||
"Set 'verify_ssl: false' to make insecure HTTPS requests."
|
||||
f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _declare_request_class(value):
|
||||
if CORE.using_esp_idf:
|
||||
return cv.declare_id(HttpRequestIDF)(value)
|
||||
if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
|
||||
return cv.declare_id(HttpRequestArduino)(value)
|
||||
return NotImplementedError
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HttpRequestComponent),
|
||||
cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
|
||||
cv.GenerateID(): _declare_request_class,
|
||||
cv.Optional(
|
||||
CONF_USERAGENT, f"ESPHome/{__version__} (https://esphome.io)"
|
||||
): cv.string,
|
||||
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
|
||||
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
|
||||
cv.Optional(
|
||||
@@ -81,12 +104,21 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
|
||||
cv.only_on_esp8266, cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All(
|
||||
cv.Any(cv.only_on_esp32, cv.only_on_rp2040),
|
||||
cv.positive_not_null_time_period,
|
||||
cv.positive_time_period_milliseconds,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.require_framework_version(
|
||||
esp8266_arduino=cv.Version(2, 5, 1),
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
esp_idf=cv.Version(0, 0, 0),
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
),
|
||||
validate_ssl_verification,
|
||||
)
|
||||
|
||||
|
||||
@@ -100,11 +132,30 @@ async def to_code(config):
|
||||
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
|
||||
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
|
||||
|
||||
if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT):
|
||||
cg.add(var.set_watchdog_timeout(timeout_ms))
|
||||
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFiClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
if CORE.using_esp_idf:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
|
||||
config.get(CONF_VERIFY_SSL),
|
||||
)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_INSECURE",
|
||||
not config.get(CONF_VERIFY_SSL),
|
||||
)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
|
||||
not config.get(CONF_VERIFY_SSL),
|
||||
)
|
||||
else:
|
||||
cg.add_library("WiFiClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266HTTPClient", None)
|
||||
if CORE.is_rp2040 and CORE.using_arduino:
|
||||
cg.add_library("HTTPClient", None)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -116,12 +167,16 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
cv.Optional(CONF_VERIFY_SSL): cv.invalid(
|
||||
f"{CONF_VERIFY_SSL} has moved to the base component configuration."
|
||||
),
|
||||
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
|
||||
),
|
||||
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
|
||||
}
|
||||
).add_extra(validate_secure_url)
|
||||
)
|
||||
HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf(
|
||||
CONF_URL,
|
||||
HTTP_REQUEST_ACTION_SCHEMA.extend(
|
||||
@@ -173,6 +228,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
|
||||
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
|
||||
cg.add(var.set_url(template_))
|
||||
cg.add(var.set_method(config[CONF_METHOD]))
|
||||
cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE]))
|
||||
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
|
||||
|
||||
if CONF_BODY in config:
|
||||
template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string)
|
||||
cg.add(var.set_body(template_))
|
||||
@@ -196,7 +254,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
cg.add(var.register_response_trigger(trigger))
|
||||
await automation.build_automation(
|
||||
trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf
|
||||
trigger,
|
||||
[
|
||||
(cg.std_shared_ptr.template(HttpContainer), "response"),
|
||||
(cg.std_string, "body"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
return var
|
||||
|
@@ -1,9 +1,8 @@
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "http_request.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
@@ -14,131 +13,12 @@ void HttpRequestComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HTTP Request:");
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_);
|
||||
ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_);
|
||||
ESP_LOGCONFIG(TAG, " Follow Redirects: %d", this->follow_redirects_);
|
||||
ESP_LOGCONFIG(TAG, " Follow redirects: %s", YESNO(this->follow_redirects_));
|
||||
ESP_LOGCONFIG(TAG, " Redirect limit: %d", this->redirect_limit_);
|
||||
}
|
||||
|
||||
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();
|
||||
if (this->watchdog_timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_);
|
||||
}
|
||||
this->client_.setReuse(true);
|
||||
}
|
||||
|
||||
void HttpRequestComponent::send(const std::vector<HttpRequestResponseTrigger *> &response_triggers) {
|
||||
if (!network::is_connected()) {
|
||||
this->client_.end();
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||
return;
|
||||
}
|
||||
|
||||
bool begin_status = false;
|
||||
const String url = this->url_.c_str();
|
||||
#if defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0))
|
||||
#if defined(USE_ESP32) || USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
|
||||
if (this->follow_redirects_) {
|
||||
this->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
||||
} else {
|
||||
this->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
|
||||
}
|
||||
#else
|
||||
this->client_.setFollowRedirects(this->follow_redirects_);
|
||||
#endif
|
||||
this->client_.setRedirectLimit(this->redirect_limit_);
|
||||
#endif
|
||||
#if defined(USE_ESP32)
|
||||
begin_status = this->client_.begin(url);
|
||||
#elif defined(USE_ESP8266)
|
||||
begin_status = this->client_.begin(*this->get_wifi_client_(), url);
|
||||
#endif
|
||||
|
||||
if (!begin_status) {
|
||||
this->client_.end();
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
this->client_.setTimeout(this->timeout_);
|
||||
#if defined(USE_ESP32)
|
||||
this->client_.setConnectTimeout(this->timeout_);
|
||||
#endif
|
||||
if (this->useragent_ != nullptr) {
|
||||
this->client_.setUserAgent(this->useragent_);
|
||||
}
|
||||
for (const auto &header : this->headers_) {
|
||||
this->client_.addHeader(header.name, header.value, false, true);
|
||||
}
|
||||
|
||||
uint32_t start_time = millis();
|
||||
int http_code = this->client_.sendRequest(this->method_, this->body_.c_str());
|
||||
uint32_t duration = millis() - start_time;
|
||||
for (auto *trigger : response_triggers)
|
||||
trigger->process(http_code, duration);
|
||||
|
||||
if (http_code < 0) {
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(),
|
||||
HTTPClient::errorToString(http_code).c_str(), duration);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (http_code < 200 || http_code >= 300) {
|
||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
std::shared_ptr<WiFiClient> HttpRequestComponent::get_wifi_client_() {
|
||||
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
if (this->secure_) {
|
||||
if (this->wifi_client_secure_ == nullptr) {
|
||||
this->wifi_client_secure_ = std::make_shared<BearSSL::WiFiClientSecure>();
|
||||
this->wifi_client_secure_->setInsecure();
|
||||
this->wifi_client_secure_->setBufferSizes(512, 512);
|
||||
}
|
||||
return this->wifi_client_secure_;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->wifi_client_ == nullptr) {
|
||||
this->wifi_client_ = std::make_shared<WiFiClient>();
|
||||
}
|
||||
return this->wifi_client_;
|
||||
}
|
||||
#endif
|
||||
|
||||
void HttpRequestComponent::close() {
|
||||
this->last_url_ = this->url_;
|
||||
this->client_.end();
|
||||
}
|
||||
|
||||
const char *HttpRequestComponent::get_string() {
|
||||
#if defined(ESP32)
|
||||
// The static variable is here because HTTPClient::getString() returns a String on ESP32,
|
||||
// and we need something to keep a buffer alive.
|
||||
static String str;
|
||||
#else
|
||||
// However on ESP8266, HTTPClient::getString() returns a String& to a member variable.
|
||||
// Leaving this the default so that any new platform either doesn't copy, or encounters a compilation error.
|
||||
auto &
|
||||
#endif
|
||||
str = this->client_.getString();
|
||||
return str.c_str();
|
||||
}
|
||||
|
||||
} // namespace http_request
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
|
@@ -1,27 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <HTTPClient.h>
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
#include <WiFiClientSecure.h>
|
||||
#endif
|
||||
#endif
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
@@ -31,9 +22,32 @@ struct Header {
|
||||
const char *value;
|
||||
};
|
||||
|
||||
class HttpRequestResponseTrigger : public Trigger<int32_t, uint32_t> {
|
||||
class HttpRequestComponent;
|
||||
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
public:
|
||||
void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); }
|
||||
virtual ~HttpContainer() = default;
|
||||
size_t content_length;
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
virtual void end() = 0;
|
||||
|
||||
void set_secure(bool secure) { this->secure_ = secure; }
|
||||
|
||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||
|
||||
protected:
|
||||
size_t bytes_read_{0};
|
||||
bool secure_{false};
|
||||
};
|
||||
|
||||
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string> {
|
||||
public:
|
||||
void process(std::shared_ptr<HttpContainer> container, std::string response_body) {
|
||||
this->trigger(std::move(container), std::move(response_body));
|
||||
}
|
||||
};
|
||||
|
||||
class HttpRequestComponent : public Component {
|
||||
@@ -41,37 +55,33 @@ class HttpRequestComponent : public Component {
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void set_url(std::string url);
|
||||
void set_method(const char *method) { this->method_ = method; }
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
|
||||
void set_body(const std::string &body) { this->body_ = body; }
|
||||
void set_headers(std::list<Header> headers) { this->headers_ = std::move(headers); }
|
||||
void send(const std::vector<HttpRequestResponseTrigger *> &response_triggers);
|
||||
void close();
|
||||
const char *get_string();
|
||||
|
||||
std::shared_ptr<HttpContainer> get(std::string url) { return this->start(std::move(url), "GET", "", {}); }
|
||||
std::shared_ptr<HttpContainer> get(std::string url, std::list<Header> headers) {
|
||||
return this->start(std::move(url), "GET", "", std::move(headers));
|
||||
}
|
||||
std::shared_ptr<HttpContainer> post(std::string url, std::string body) {
|
||||
return this->start(std::move(url), "POST", std::move(body), {});
|
||||
}
|
||||
std::shared_ptr<HttpContainer> post(std::string url, std::string body, std::list<Header> headers) {
|
||||
return this->start(std::move(url), "POST", std::move(body), std::move(headers));
|
||||
}
|
||||
|
||||
virtual std::shared_ptr<HttpContainer> start(std::string url, std::string method, std::string body,
|
||||
std::list<Header> headers) = 0;
|
||||
|
||||
protected:
|
||||
HTTPClient client_{};
|
||||
std::string url_;
|
||||
std::string last_url_;
|
||||
const char *method_;
|
||||
const char *useragent_{nullptr};
|
||||
bool secure_;
|
||||
bool follow_redirects_;
|
||||
uint16_t redirect_limit_;
|
||||
uint16_t timeout_{5000};
|
||||
std::string body_;
|
||||
std::list<Header> headers_;
|
||||
#ifdef USE_ESP8266
|
||||
std::shared_ptr<WiFiClient> wifi_client_;
|
||||
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
std::shared_ptr<BearSSL::WiFiClientSecure> wifi_client_secure_;
|
||||
#endif
|
||||
std::shared_ptr<WiFiClient> get_wifi_client_();
|
||||
#endif
|
||||
uint32_t watchdog_timeout_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
@@ -80,6 +90,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
TEMPLATABLE_VALUE(std::string, url)
|
||||
TEMPLATABLE_VALUE(const char *, method)
|
||||
TEMPLATABLE_VALUE(std::string, body)
|
||||
TEMPLATABLE_VALUE(bool, capture_response)
|
||||
|
||||
void add_header(const char *key, TemplatableValue<const char *, Ts...> value) { this->headers_.insert({key, value}); }
|
||||
|
||||
@@ -89,19 +100,22 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
|
||||
void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); }
|
||||
|
||||
void set_max_response_buffer_size(size_t max_response_buffer_size) {
|
||||
this->max_response_buffer_size_ = max_response_buffer_size;
|
||||
}
|
||||
|
||||
void play(Ts... x) override {
|
||||
this->parent_->set_url(this->url_.value(x...));
|
||||
this->parent_->set_method(this->method_.value(x...));
|
||||
std::string body;
|
||||
if (this->body_.has_value()) {
|
||||
this->parent_->set_body(this->body_.value(x...));
|
||||
body = this->body_.value(x...);
|
||||
}
|
||||
if (!this->json_.empty()) {
|
||||
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
|
||||
this->parent_->set_body(json::build_json(f));
|
||||
body = json::build_json(f);
|
||||
}
|
||||
if (this->json_func_ != nullptr) {
|
||||
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
|
||||
this->parent_->set_body(json::build_json(f));
|
||||
body = json::build_json(f);
|
||||
}
|
||||
std::list<Header> headers;
|
||||
for (const auto &item : this->headers_) {
|
||||
@@ -111,10 +125,37 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
header.value = val.value(x...);
|
||||
headers.push_back(header);
|
||||
}
|
||||
this->parent_->set_headers(headers);
|
||||
this->parent_->send(this->response_triggers_);
|
||||
this->parent_->close();
|
||||
this->parent_->set_body("");
|
||||
|
||||
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers);
|
||||
|
||||
if (container == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t content_length = container->content_length;
|
||||
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
|
||||
|
||||
std::string response_body;
|
||||
if (this->capture_response_.value(x...)) {
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
read_index += read;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *trigger : this->response_triggers_) {
|
||||
trigger->process(container, response_body);
|
||||
}
|
||||
container->end();
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -130,9 +171,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
|
||||
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
|
||||
std::vector<HttpRequestResponseTrigger *> response_triggers_;
|
||||
|
||||
size_t max_response_buffer_size_{SIZE_MAX};
|
||||
};
|
||||
|
||||
} // namespace http_request
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
|
161
esphome/components/http_request/http_request_arduino.cpp
Normal file
161
esphome/components/http_request/http_request_arduino.cpp
Normal 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
|
40
esphome/components/http_request/http_request_arduino.h
Normal file
40
esphome/components/http_request/http_request_arduino.h
Normal 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
|
155
esphome/components/http_request/http_request_idf.cpp
Normal file
155
esphome/components/http_request/http_request_idf.cpp
Normal 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
|
34
esphome/components/http_request/http_request_idf.h
Normal file
34
esphome/components/http_request/http_request_idf.h
Normal 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
|
@@ -2,92 +2,35 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.const import (
|
||||
CONF_ESP8266_DISABLE_SSL_SUPPORT,
|
||||
CONF_ID,
|
||||
CONF_PASSWORD,
|
||||
CONF_TIMEOUT,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from esphome.components import esp32
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from .. import http_request_ns
|
||||
from esphome.core import coroutine_with_priority
|
||||
from .. import CONF_HTTP_REQUEST_ID, http_request_ns, HttpRequestComponent
|
||||
|
||||
CODEOWNERS = ["@oarcher"]
|
||||
|
||||
AUTO_LOAD = ["md5"]
|
||||
DEPENDENCIES = ["network"]
|
||||
DEPENDENCIES = ["network", "http_request"]
|
||||
|
||||
CONF_MD5 = "md5"
|
||||
CONF_MD5_URL = "md5_url"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
|
||||
|
||||
OtaHttpRequestComponent = http_request_ns.class_(
|
||||
"OtaHttpRequestComponent", OTAComponent
|
||||
)
|
||||
OtaHttpRequestComponentArduino = http_request_ns.class_(
|
||||
"OtaHttpRequestComponentArduino", OtaHttpRequestComponent
|
||||
)
|
||||
OtaHttpRequestComponentIDF = http_request_ns.class_(
|
||||
"OtaHttpRequestComponentIDF", OtaHttpRequestComponent
|
||||
)
|
||||
OtaHttpRequestComponentFlashAction = http_request_ns.class_(
|
||||
"OtaHttpRequestComponentFlashAction", automation.Action
|
||||
)
|
||||
|
||||
|
||||
def validate_ssl_verification(config):
|
||||
error_message = ""
|
||||
|
||||
if CORE.is_esp32:
|
||||
if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
|
||||
error_message = "ESPHome supports certificate verification only via ESP-IDF"
|
||||
|
||||
if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
|
||||
error_message = "ESPHome does not support certificate verification in Arduino"
|
||||
|
||||
if (
|
||||
CORE.is_esp8266
|
||||
and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]
|
||||
and config[CONF_VERIFY_SSL]
|
||||
):
|
||||
error_message = "ESPHome does not support certificate verification in Arduino"
|
||||
|
||||
if len(error_message) > 0:
|
||||
raise cv.Invalid(
|
||||
f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections."
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _declare_request_class(value):
|
||||
if CORE.using_esp_idf:
|
||||
return cv.declare_id(OtaHttpRequestComponentIDF)(value)
|
||||
|
||||
if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040:
|
||||
return cv.declare_id(OtaHttpRequestComponentArduino)(value)
|
||||
return NotImplementedError
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): _declare_request_class,
|
||||
cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All(
|
||||
cv.only_on_esp8266, cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_TIMEOUT, default="5min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All(
|
||||
cv.Any(cv.only_on_esp32, cv.only_on_rp2040),
|
||||
cv.positive_not_null_time_period,
|
||||
cv.positive_time_period_milliseconds,
|
||||
),
|
||||
cv.GenerateID(): cv.declare_id(OtaHttpRequestComponent),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
}
|
||||
)
|
||||
.extend(BASE_OTA_SCHEMA)
|
||||
@@ -98,7 +41,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp_idf=cv.Version(0, 0, 0),
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
),
|
||||
validate_ssl_verification,
|
||||
)
|
||||
|
||||
|
||||
@@ -106,41 +48,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ota_to_code(var, config)
|
||||
|
||||
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||
|
||||
if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT):
|
||||
cg.add_define(
|
||||
"USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT",
|
||||
timeout_ms,
|
||||
)
|
||||
|
||||
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
|
||||
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
|
||||
|
||||
if CORE.is_esp32:
|
||||
if CORE.using_esp_idf:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
|
||||
config.get(CONF_VERIFY_SSL),
|
||||
)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_INSECURE",
|
||||
not config.get(CONF_VERIFY_SSL),
|
||||
)
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY",
|
||||
not config.get(CONF_VERIFY_SSL),
|
||||
)
|
||||
else:
|
||||
cg.add_library("WiFiClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266HTTPClient", None)
|
||||
if CORE.is_rp2040 and CORE.using_arduino:
|
||||
cg.add_library("HTTPClient", None)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
|
||||
|
||||
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
|
||||
@@ -148,7 +57,9 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(OtaHttpRequestComponent),
|
||||
cv.Optional(CONF_MD5_URL): cv.templatable(cv.url),
|
||||
cv.Optional(CONF_MD5): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_MD5): cv.templatable(
|
||||
cv.All(cv.string, cv.Length(min=32, max=32))
|
||||
),
|
||||
cv.Optional(CONF_PASSWORD): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_USERNAME): cv.templatable(cv.string),
|
||||
cv.Required(CONF_URL): cv.templatable(cv.url),
|
||||
@@ -159,7 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ota_http_request.flash",
|
||||
"ota.http_request.flash",
|
||||
OtaHttpRequestComponentFlashAction,
|
||||
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
|
||||
)
|
||||
|
@@ -1,45 +1,29 @@
|
||||
#include "ota_http_request.h"
|
||||
#include "watchdog.h"
|
||||
#include "../watchdog.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "esphome/components/md5/md5.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/components/ota/ota_backend_arduino_esp32.h"
|
||||
#include "esphome/components/ota/ota_backend_arduino_esp8266.h"
|
||||
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
|
||||
#include "esphome/components/ota/ota_backend_esp_idf.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.ota";
|
||||
|
||||
void OtaHttpRequestComponent::setup() {
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
ota::register_ota_platform(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request:");
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %llus", this->timeout_ / 1000);
|
||||
#ifdef USE_ESP8266
|
||||
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
ESP_LOGCONFIG(TAG, " ESP8266 SSL support: No");
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " ESP8266 SSL support: Yes");
|
||||
#endif
|
||||
#endif
|
||||
#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
ESP_LOGCONFIG(TAG, " TLS server verification: Yes");
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " TLS server verification: No");
|
||||
#endif
|
||||
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
|
||||
ESP_LOGCONFIG(TAG, " Watchdog timeout: %ds", USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT / 1000);
|
||||
#endif
|
||||
};
|
||||
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
|
||||
|
||||
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
|
||||
if (!this->validate_url_(url)) {
|
||||
@@ -58,20 +42,6 @@ void OtaHttpRequestComponent::set_url(const std::string &url) {
|
||||
this->url_ = url;
|
||||
}
|
||||
|
||||
bool OtaHttpRequestComponent::check_status() {
|
||||
// status can be -1, or HTTP status code
|
||||
if (this->status_ < 100) {
|
||||
ESP_LOGE(TAG, "HTTP server did not respond (error %d)", this->status_);
|
||||
return false;
|
||||
}
|
||||
if (this->status_ >= 310) {
|
||||
ESP_LOGE(TAG, "HTTP error %d", this->status_);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "HTTP status %d", this->status_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::flash() {
|
||||
if (this->url_.empty()) {
|
||||
ESP_LOGE(TAG, "URL not set; cannot start update");
|
||||
@@ -104,17 +74,18 @@ void OtaHttpRequestComponent::flash() {
|
||||
}
|
||||
}
|
||||
|
||||
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend) {
|
||||
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
|
||||
const std::shared_ptr<HttpContainer> &container) {
|
||||
if (this->update_started_) {
|
||||
ESP_LOGV(TAG, "Aborting OTA backend");
|
||||
backend->abort();
|
||||
}
|
||||
ESP_LOGV(TAG, "Aborting HTTP connection");
|
||||
this->http_end();
|
||||
container->end();
|
||||
};
|
||||
|
||||
uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
uint8_t buf[this->http_recv_buffer_ + 1];
|
||||
uint8_t buf[OtaHttpRequestComponent::HTTP_RECV_BUFFER + 1];
|
||||
uint32_t last_progress = 0;
|
||||
uint32_t update_start_time = millis();
|
||||
md5::MD5Digest md5_receive;
|
||||
@@ -132,9 +103,10 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
}
|
||||
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
|
||||
ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
|
||||
this->http_init(url_with_auth);
|
||||
if (!this->check_status()) {
|
||||
this->http_end();
|
||||
|
||||
auto container = this->parent_->get(url_with_auth);
|
||||
|
||||
if (container == nullptr) {
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
|
||||
@@ -144,18 +116,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
|
||||
ESP_LOGV(TAG, "OTA backend begin");
|
||||
auto backend = ota::make_ota_backend();
|
||||
auto error_code = backend->begin(this->body_length_);
|
||||
auto error_code = backend->begin(container->content_length);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "backend->begin error: %d", error_code);
|
||||
this->cleanup_(std::move(backend));
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
|
||||
this->bytes_read_ = 0;
|
||||
while (this->bytes_read_ < this->body_length_) {
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned)
|
||||
int bufsize = this->http_read(buf, this->http_recv_buffer_);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", this->bytes_read_, this->body_length_, bufsize);
|
||||
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize);
|
||||
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
@@ -163,9 +135,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed");
|
||||
this->cleanup_(std::move(backend));
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
} else if (bufsize > 0 && bufsize <= this->http_recv_buffer_) {
|
||||
} else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize);
|
||||
|
||||
@@ -176,16 +148,16 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
this->bytes_read_ - bufsize, this->body_length_);
|
||||
this->cleanup_(std::move(backend));
|
||||
container->get_bytes_read() - bufsize, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
if ((now - last_progress > 1000) or (this->bytes_read_ == this->body_length_)) {
|
||||
if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
|
||||
last_progress = now;
|
||||
float percentage = this->bytes_read_ * 100.0f / this->body_length_;
|
||||
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
|
||||
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
@@ -201,13 +173,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
this->md5_computed_ = md5_receive_str.get();
|
||||
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
|
||||
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
|
||||
this->cleanup_(std::move(backend));
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
} else {
|
||||
backend->set_update_md5(md5_receive_str.get());
|
||||
}
|
||||
|
||||
this->http_end();
|
||||
container->end();
|
||||
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
@@ -217,7 +189,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
error_code = backend->end();
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
|
||||
this->cleanup_(std::move(backend));
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
|
||||
@@ -256,28 +228,32 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
|
||||
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
|
||||
ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
|
||||
this->http_init(url_with_auth);
|
||||
if (!this->check_status()) {
|
||||
this->http_end();
|
||||
auto container = this->parent_->get(url_with_auth);
|
||||
if (container == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to connect to MD5 URL");
|
||||
return false;
|
||||
}
|
||||
int length = this->body_length_;
|
||||
if (length < 0) {
|
||||
this->http_end();
|
||||
size_t length = container->content_length;
|
||||
if (length == 0) {
|
||||
container->end();
|
||||
return false;
|
||||
}
|
||||
if (length < MD5_SIZE) {
|
||||
ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE,
|
||||
this->body_length_);
|
||||
this->http_end();
|
||||
ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
|
||||
container->end();
|
||||
return false;
|
||||
}
|
||||
|
||||
this->bytes_read_ = 0;
|
||||
this->md5_expected_.resize(MD5_SIZE);
|
||||
auto read_len = this->http_read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
this->http_end();
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
container->end();
|
||||
|
||||
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
|
||||
return read_len == MD5_SIZE;
|
||||
}
|
||||
|
||||
|
@@ -1,17 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "../http_request.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.ota";
|
||||
static const uint8_t MD5_SIZE = 32;
|
||||
|
||||
enum OtaHttpRequestError : uint8_t {
|
||||
@@ -20,7 +22,7 @@ enum OtaHttpRequestError : uint8_t {
|
||||
OTA_CONNECTION_ERROR = 0x12,
|
||||
};
|
||||
|
||||
class OtaHttpRequestComponent : public ota::OTAComponent {
|
||||
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -29,27 +31,19 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
|
||||
void set_md5_url(const std::string &md5_url);
|
||||
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
|
||||
void set_password(const std::string &password) { this->password_ = password; }
|
||||
void set_timeout(const uint64_t timeout) { this->timeout_ = timeout; }
|
||||
void set_url(const std::string &url);
|
||||
void set_username(const std::string &username) { this->username_ = username; }
|
||||
|
||||
std::string md5_computed() { return this->md5_computed_; }
|
||||
std::string md5_expected() { return this->md5_expected_; }
|
||||
|
||||
bool check_status();
|
||||
|
||||
void flash();
|
||||
|
||||
virtual void http_init(const std::string &url){};
|
||||
virtual int http_read(uint8_t *buf, size_t len) { return 0; };
|
||||
virtual void http_end(){};
|
||||
|
||||
protected:
|
||||
void cleanup_(std::unique_ptr<ota::OTABackend> backend);
|
||||
void cleanup_(std::unique_ptr<ota::OTABackend> backend, const std::shared_ptr<HttpContainer> &container);
|
||||
uint8_t do_ota_();
|
||||
std::string get_url_with_auth_(const std::string &url);
|
||||
bool http_get_md5_();
|
||||
bool secure_() { return this->url_.find("https:") != std::string::npos; };
|
||||
bool validate_url_(const std::string &url);
|
||||
|
||||
std::string md5_computed_{};
|
||||
@@ -58,14 +52,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent {
|
||||
std::string password_{};
|
||||
std::string username_{};
|
||||
std::string url_{};
|
||||
size_t body_length_ = 0;
|
||||
size_t bytes_read_ = 0;
|
||||
int status_ = -1;
|
||||
uint64_t timeout_ = 0;
|
||||
bool update_started_ = false;
|
||||
const uint16_t http_recv_buffer_ = 256; // the firmware GET chunk size
|
||||
const uint16_t max_http_recv_buffer_ = 512; // internal max http buffer size must be > HTTP_RECV_BUFFER_ (TLS
|
||||
// overhead) and must be a power of two from 512 to 4096
|
||||
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
|
||||
};
|
||||
|
||||
} // namespace http_request
|
||||
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
44
esphome/components/http_request/update/__init__.py
Normal file
44
esphome/components/http_request/update/__init__.py
Normal 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)
|
157
esphome/components/http_request/update/http_request_update.cpp
Normal file
157
esphome/components/http_request/update/http_request_update.cpp
Normal 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
|
37
esphome/components/http_request/update/http_request_update.h
Normal file
37
esphome/components/http_request/update/http_request_update.h
Normal 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 ¤t_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
|
@@ -1,7 +1,5 @@
|
||||
#include "watchdog.h"
|
||||
|
||||
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -20,14 +18,22 @@ namespace esphome {
|
||||
namespace http_request {
|
||||
namespace watchdog {
|
||||
|
||||
static const char *const TAG = "watchdog.http_request.ota";
|
||||
static const char *const TAG = "http_request.watchdog";
|
||||
|
||||
WatchdogManager::WatchdogManager() {
|
||||
WatchdogManager::WatchdogManager(uint32_t timeout_ms) : timeout_ms_(timeout_ms) {
|
||||
if (timeout_ms == 0) {
|
||||
return;
|
||||
}
|
||||
this->saved_timeout_ms_ = this->get_timeout_();
|
||||
this->set_timeout_(USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT);
|
||||
this->set_timeout_(timeout_ms);
|
||||
}
|
||||
|
||||
WatchdogManager::~WatchdogManager() { this->set_timeout_(this->saved_timeout_ms_); }
|
||||
WatchdogManager::~WatchdogManager() {
|
||||
if (this->timeout_ms_ == 0) {
|
||||
return;
|
||||
}
|
||||
this->set_timeout_(this->saved_timeout_ms_);
|
||||
}
|
||||
|
||||
void WatchdogManager::set_timeout_(uint32_t timeout_ms) {
|
||||
ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms);
|
||||
@@ -68,4 +74,3 @@ uint32_t WatchdogManager::get_timeout_() {
|
||||
} // namespace watchdog
|
||||
} // namespace http_request
|
||||
} // namespace esphome
|
||||
#endif
|
@@ -9,9 +9,8 @@ namespace http_request {
|
||||
namespace watchdog {
|
||||
|
||||
class WatchdogManager {
|
||||
#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT
|
||||
public:
|
||||
WatchdogManager();
|
||||
WatchdogManager(uint32_t timeout_ms);
|
||||
~WatchdogManager();
|
||||
|
||||
private:
|
||||
@@ -19,7 +18,7 @@ class WatchdogManager {
|
||||
void set_timeout_(uint32_t timeout_ms);
|
||||
|
||||
uint32_t saved_timeout_ms_{0};
|
||||
#endif
|
||||
uint32_t timeout_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace watchdog
|
@@ -27,43 +27,24 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
this->start();
|
||||
}
|
||||
}
|
||||
|
||||
if (play_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) {
|
||||
this->is_announcement_ = true;
|
||||
}
|
||||
|
||||
if (call.get_volume().has_value()) {
|
||||
this->volume = call.get_volume().value();
|
||||
this->set_volume_(volume);
|
||||
this->unmute_();
|
||||
}
|
||||
if (this->i2s_state_ != I2S_STATE_RUNNING) {
|
||||
return;
|
||||
}
|
||||
if (call.get_command().has_value()) {
|
||||
switch (call.get_command().value()) {
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
if (!this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = play_state;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_STOP:
|
||||
this->stop();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
|
||||
this->mute_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
this->unmute_();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
this->audio_->pauseResume();
|
||||
if (this->audio_->isRunning()) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
} else {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
}
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
|
||||
float new_volume = this->volume + 0.1f;
|
||||
if (new_volume > 1.0f)
|
||||
@@ -80,6 +61,36 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
this->unmute_();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (this->i2s_state_ != I2S_STATE_RUNNING) {
|
||||
return;
|
||||
}
|
||||
switch (call.get_command().value()) {
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
if (!this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = play_state;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
|
||||
if (this->audio_->isRunning())
|
||||
this->audio_->pauseResume();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_STOP:
|
||||
this->stop();
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
this->audio_->pauseResume();
|
||||
if (this->audio_->isRunning()) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
} else {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->publish_state();
|
||||
@@ -171,9 +182,8 @@ void I2SAudioMediaPlayer::start_() {
|
||||
if (this->current_url_.has_value()) {
|
||||
this->audio_->connecttohost(this->current_url_.value().c_str());
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
if (this->is_announcement_.has_value()) {
|
||||
this->state = this->is_announcement_.value() ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING
|
||||
: media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
if (this->is_announcement_) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING;
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
@@ -202,6 +212,7 @@ void I2SAudioMediaPlayer::stop_() {
|
||||
this->high_freq_.stop();
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
this->publish_state();
|
||||
this->is_announcement_ = false;
|
||||
}
|
||||
|
||||
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
|
||||
|
@@ -78,7 +78,7 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer,
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
optional<std::string> current_url_{};
|
||||
optional<bool> is_announcement_{};
|
||||
bool is_announcement_{false};
|
||||
};
|
||||
|
||||
} // namespace i2s_audio
|
||||
|
@@ -38,15 +38,22 @@ void I2SAudioSpeaker::start() {
|
||||
ESP_LOGE(TAG, "Cannot start audio, speaker failed to setup");
|
||||
return;
|
||||
}
|
||||
if (this->task_created_) {
|
||||
ESP_LOGW(TAG, "Called start while task has been already created.");
|
||||
return;
|
||||
}
|
||||
this->state_ = speaker::STATE_STARTING;
|
||||
}
|
||||
void I2SAudioSpeaker::start_() {
|
||||
if (this->task_created_) {
|
||||
return;
|
||||
}
|
||||
if (!this->parent_->try_lock()) {
|
||||
return; // Waiting for another i2s component to return lock
|
||||
}
|
||||
this->state_ = speaker::STATE_RUNNING;
|
||||
|
||||
xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 1, &this->player_task_handle_);
|
||||
this->task_created_ = true;
|
||||
}
|
||||
|
||||
void I2SAudioSpeaker::player_task(void *params) {
|
||||
@@ -131,7 +138,16 @@ void I2SAudioSpeaker::player_task(void *params) {
|
||||
(10 / portTICK_PERIOD_MS));
|
||||
if (err != ESP_OK) {
|
||||
event = {.type = TaskEventType::WARNING, .err = err};
|
||||
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
|
||||
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "Failed to send WARNING event");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (bytes_written != sizeof(sample)) {
|
||||
event = {.type = TaskEventType::WARNING, .err = ESP_FAIL};
|
||||
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "Failed to send WARNING event");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
remaining--;
|
||||
@@ -139,18 +155,25 @@ void I2SAudioSpeaker::player_task(void *params) {
|
||||
}
|
||||
|
||||
event.type = TaskEventType::PLAYING;
|
||||
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
|
||||
event.err = current;
|
||||
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "Failed to send PLAYING event");
|
||||
}
|
||||
}
|
||||
|
||||
event.type = TaskEventType::STOPPING;
|
||||
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "Failed to send STOPPING event");
|
||||
}
|
||||
|
||||
i2s_zero_dma_buffer(this_speaker->parent_->get_port());
|
||||
|
||||
event.type = TaskEventType::STOPPING;
|
||||
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
|
||||
|
||||
i2s_driver_uninstall(this_speaker->parent_->get_port());
|
||||
|
||||
event.type = TaskEventType::STOPPED;
|
||||
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
|
||||
if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "Failed to send STOPPED event");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
delay(10);
|
||||
@@ -181,6 +204,7 @@ void I2SAudioSpeaker::watch_() {
|
||||
break;
|
||||
case TaskEventType::STARTED:
|
||||
ESP_LOGD(TAG, "Started I2S Audio Speaker");
|
||||
this->state_ = speaker::STATE_RUNNING;
|
||||
break;
|
||||
case TaskEventType::STOPPING:
|
||||
ESP_LOGD(TAG, "Stopping I2S Audio Speaker");
|
||||
@@ -191,6 +215,7 @@ void I2SAudioSpeaker::watch_() {
|
||||
case TaskEventType::STOPPED:
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
vTaskDelete(this->player_task_handle_);
|
||||
this->task_created_ = false;
|
||||
this->player_task_handle_ = nullptr;
|
||||
this->parent_->unlock();
|
||||
xQueueReset(this->buffer_queue_);
|
||||
@@ -208,7 +233,6 @@ void I2SAudioSpeaker::loop() {
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING:
|
||||
this->start_();
|
||||
break;
|
||||
case speaker::STATE_RUNNING:
|
||||
case speaker::STATE_STOPPING:
|
||||
this->watch_();
|
||||
|
@@ -60,7 +60,6 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
|
||||
|
||||
protected:
|
||||
void start_();
|
||||
// void stop_();
|
||||
void watch_();
|
||||
|
||||
static void player_task(void *params);
|
||||
@@ -70,6 +69,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud
|
||||
QueueHandle_t event_queue_;
|
||||
|
||||
uint8_t dout_pin_{0};
|
||||
bool task_created_{false};
|
||||
|
||||
#if SOC_I2S_SUPPORTS_DAC
|
||||
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
|
||||
|
@@ -69,6 +69,7 @@ MODELS = {
|
||||
"ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay),
|
||||
"ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay),
|
||||
"ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay),
|
||||
"ST7735": ili9xxx_ns.class_("ILI9XXXST7735", ILI9XXXDisplay),
|
||||
"ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay),
|
||||
"ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay),
|
||||
"S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay),
|
||||
@@ -134,6 +135,7 @@ def _validate(config):
|
||||
"ILI9341",
|
||||
"ILI9342",
|
||||
"ST7789V",
|
||||
"ST7735",
|
||||
]:
|
||||
raise cv.Invalid("Selected model can't run on ESP8266.")
|
||||
|
||||
|
@@ -70,6 +70,7 @@ static const uint8_t ILI9XXX_PWCTR2 = 0xC1;
|
||||
static const uint8_t ILI9XXX_PWCTR3 = 0xC2;
|
||||
static const uint8_t ILI9XXX_PWCTR4 = 0xC3;
|
||||
static const uint8_t ILI9XXX_PWCTR5 = 0xC4;
|
||||
static const uint8_t ILI9XXX_PWCTR6 = 0xF6;
|
||||
static const uint8_t ILI9XXX_VMCTR1 = 0xC5;
|
||||
static const uint8_t ILI9XXX_IFCTR = 0xC6;
|
||||
static const uint8_t ILI9XXX_VMCTR2 = 0xC7;
|
||||
@@ -91,6 +92,7 @@ static const uint8_t ILI9XXX_GMCTRN1 = 0xE1;
|
||||
|
||||
static const uint8_t ILI9XXX_CSCON = 0xF0;
|
||||
static const uint8_t ILI9XXX_ADJCTL3 = 0xF7;
|
||||
static const uint8_t ILI9XXX_DELAY = 0xFF; // followed by one byte of delay time in ms
|
||||
|
||||
} // namespace ili9xxx
|
||||
} // namespace esphome
|
||||
|
@@ -411,11 +411,19 @@ void ILI9XXXDisplay::init_lcd_(const uint8_t *addr) {
|
||||
uint8_t cmd, x, num_args;
|
||||
while ((cmd = *addr++) != 0) {
|
||||
x = *addr++;
|
||||
num_args = x & 0x7F;
|
||||
this->send_command(cmd, addr, num_args);
|
||||
addr += num_args;
|
||||
if (x & 0x80)
|
||||
delay(150); // NOLINT
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,6 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
while ((cmd = *addr++) != 0) {
|
||||
num_args = *addr++ & 0x7F;
|
||||
bits = *addr;
|
||||
esph_log_d(TAG, "Command %02X, length %d, bits %02X", cmd, num_args, bits);
|
||||
switch (cmd) {
|
||||
case ILI9XXX_MADCTL: {
|
||||
this->swap_xy_ = (bits & MADCTL_MV) != 0;
|
||||
@@ -51,6 +50,9 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
break;
|
||||
}
|
||||
|
||||
case ILI9XXX_DELAY:
|
||||
continue; // no args to skip
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -269,5 +271,11 @@ class ILI9XXXGC9A01A : public ILI9XXXDisplay {
|
||||
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {}
|
||||
};
|
||||
|
||||
//----------- ILI9XXX_24_TFT display --------------
|
||||
class ILI9XXXST7735 : public ILI9XXXDisplay {
|
||||
public:
|
||||
ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160, false) {}
|
||||
};
|
||||
|
||||
} // namespace ili9xxx
|
||||
} // namespace esphome
|
||||
|
@@ -370,6 +370,57 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = {
|
||||
0x00 // End of list
|
||||
};
|
||||
|
||||
static const uint8_t PROGMEM INITCMD_ST7735[] = {
|
||||
ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms
|
||||
ILI9XXX_DELAY, 10,
|
||||
ILI9XXX_SLPOUT , 0, // Exit Sleep, delay
|
||||
ILI9XXX_DELAY, 10,
|
||||
ILI9XXX_PIXFMT , 1, 0x05,
|
||||
ILI9XXX_FRMCTR1, 3, // 4: Frame rate control, 3 args + delay:
|
||||
0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D)
|
||||
ILI9XXX_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args:
|
||||
0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D)
|
||||
ILI9XXX_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args:
|
||||
0x01, 0x2C, 0x2D, // Dot inversion mode
|
||||
0x01, 0x2C, 0x2D, // Line inversion mode
|
||||
|
||||
ILI9XXX_INVCTR, 1, // 7: Display inversion control, 1 arg:
|
||||
0x7, // Line inversion
|
||||
ILI9XXX_PWCTR1, 3, // 7: Power control, 3 args, no delay:
|
||||
0xA2,
|
||||
0x02, // -4.6V
|
||||
0x84, // AUTO mode
|
||||
ILI9XXX_PWCTR2, 1, // 8: Power control, 1 arg, no delay:
|
||||
0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD
|
||||
ILI9XXX_PWCTR3, 2, // 9: Power control, 2 args, no delay:
|
||||
0x0A, // Opamp current small
|
||||
0x00, // Boost frequency
|
||||
ILI9XXX_PWCTR4, 2, // 10: Power control, 2 args, no delay:
|
||||
0x8A, // BCLK/2,
|
||||
0x2A, // opamp current small & medium low
|
||||
ILI9XXX_PWCTR5, 2, // 11: Power control, 2 args, no delay:
|
||||
0x8A, 0xEE,
|
||||
|
||||
ILI9XXX_VMCTR1, 1, // 11: Power control, 2 args + delay:
|
||||
0x0E,
|
||||
ILI9XXX_GMCTRP1, 16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay:
|
||||
0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides
|
||||
0x37, 0x32, 0x29, 0x2d, // accurate colors)
|
||||
0x29, 0x25, 0x2B, 0x39,
|
||||
0x00, 0x01, 0x03, 0x10,
|
||||
ILI9XXX_GMCTRN1, 16, // 14: Gamma Adjustments (neg. polarity), 16 args + delay:
|
||||
0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides
|
||||
0x2E, 0x2C, 0x29, 0x2D, // accurate colors)
|
||||
0x2E, 0x2E, 0x37, 0x3F,
|
||||
0x00, 0x00, 0x02, 0x10,
|
||||
ILI9XXX_MADCTL , 1, 0x00, // Memory Access Control, BGR
|
||||
ILI9XXX_NORON , 0,
|
||||
ILI9XXX_DELAY, 10,
|
||||
ILI9XXX_DISPON , 0, // Display on
|
||||
ILI9XXX_DELAY, 10,
|
||||
00, // endo of list
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
} // namespace ili9xxx
|
||||
} // namespace esphome
|
||||
|
@@ -9,8 +9,6 @@ import re
|
||||
import requests
|
||||
from magic import Magic
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from esphome import core
|
||||
from esphome.components import font
|
||||
from esphome import external_files
|
||||
@@ -68,7 +66,7 @@ def _compute_local_icon_path(value: dict) -> Path:
|
||||
return base_dir / f"{value[CONF_ICON]}.svg"
|
||||
|
||||
|
||||
def _compute_local_image_path(value: dict) -> Path:
|
||||
def compute_local_image_path(value: dict) -> Path:
|
||||
url = value[CONF_URL]
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
@@ -117,7 +115,7 @@ def download_mdi(value):
|
||||
|
||||
def download_image(value):
|
||||
url = value[CONF_URL]
|
||||
path = _compute_local_image_path(value)
|
||||
path = compute_local_image_path(value)
|
||||
|
||||
download_content(url, path)
|
||||
|
||||
@@ -267,6 +265,9 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
|
||||
|
||||
|
||||
def load_svg_image(file: bytes, resize: tuple[int, int]):
|
||||
# Local import only to allow "validate_pillow_installed" to run *before* importing it
|
||||
from PIL import Image
|
||||
|
||||
# This import is only needed in case of SVG images; adding it
|
||||
# to the top would force configurations not using SVG to also have it
|
||||
# installed for no reason.
|
||||
@@ -286,6 +287,9 @@ def load_svg_image(file: bytes, resize: tuple[int, int]):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Local import only to allow "validate_pillow_installed" to run *before* importing it
|
||||
from PIL import Image
|
||||
|
||||
conf_file = config[CONF_FILE]
|
||||
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
@@ -295,7 +299,7 @@ async def to_code(config):
|
||||
path = _compute_local_icon_path(conf_file).as_posix()
|
||||
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||
path = _compute_local_image_path(conf_file).as_posix()
|
||||
path = compute_local_image_path(conf_file).as_posix()
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
|
@@ -62,7 +62,7 @@ std::string build_json(const json_build_t &f) {
|
||||
}
|
||||
}
|
||||
|
||||
void parse_json(const std::string &data, const json_parse_t &f) {
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// Here we are allocating 1.5 times the data size,
|
||||
// with the heap size minus 2kb to be safe if less than that
|
||||
// as we can not have a true dynamic sized document.
|
||||
@@ -76,14 +76,13 @@ void parse_json(const std::string &data, const json_parse_t &f) {
|
||||
#elif defined(USE_LIBRETINY)
|
||||
const size_t free_heap = lt_heap_get_free();
|
||||
#endif
|
||||
bool pass = false;
|
||||
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
|
||||
do {
|
||||
while (true) {
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
|
||||
free_heap);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
DeserializationError err = deserializeJson(json_document, data);
|
||||
json_document.shrinkToFit();
|
||||
@@ -91,21 +90,21 @@ void parse_json(const std::string &data, const json_parse_t &f) {
|
||||
JsonObject root = json_document.as<JsonObject>();
|
||||
|
||||
if (err == DeserializationError::Ok) {
|
||||
pass = true;
|
||||
f(root);
|
||||
return f(root);
|
||||
} else if (err == DeserializationError::NoMemory) {
|
||||
if (request_size * 2 >= free_heap) {
|
||||
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Increasing memory allocation.");
|
||||
request_size *= 2;
|
||||
continue;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "JSON parse error: %s", err.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
} while (!pass);
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
@@ -14,7 +14,7 @@ namespace esphome {
|
||||
namespace json {
|
||||
|
||||
/// Callback function typedef for parsing JsonObjects.
|
||||
using json_parse_t = std::function<void(JsonObject)>;
|
||||
using json_parse_t = std::function<bool(JsonObject)>;
|
||||
|
||||
/// Callback function typedef for building JsonObjects.
|
||||
using json_build_t = std::function<void(JsonObject)>;
|
||||
@@ -23,7 +23,7 @@ using json_build_t = std::function<void(JsonObject)>;
|
||||
std::string build_json(const json_build_t &f);
|
||||
|
||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||
void parse_json(const std::string &data, const json_parse_t &f);
|
||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||
|
||||
} // namespace json
|
||||
} // namespace esphome
|
||||
|
@@ -112,11 +112,18 @@ HARDWARE_UART_TO_UART_SELECTION = {
|
||||
}
|
||||
|
||||
HARDWARE_UART_TO_SERIAL = {
|
||||
UART0: cg.global_ns.Serial,
|
||||
UART0_SWAP: cg.global_ns.Serial,
|
||||
UART1: cg.global_ns.Serial1,
|
||||
UART2: cg.global_ns.Serial2,
|
||||
DEFAULT: cg.global_ns.Serial,
|
||||
PLATFORM_ESP8266: {
|
||||
UART0: cg.global_ns.Serial,
|
||||
UART0_SWAP: cg.global_ns.Serial,
|
||||
UART1: cg.global_ns.Serial1,
|
||||
UART2: cg.global_ns.Serial2,
|
||||
DEFAULT: cg.global_ns.Serial,
|
||||
},
|
||||
PLATFORM_RP2040: {
|
||||
UART0: cg.global_ns.Serial1,
|
||||
UART1: cg.global_ns.Serial2,
|
||||
USB_CDC: cg.global_ns.Serial,
|
||||
},
|
||||
}
|
||||
|
||||
is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
|
||||
@@ -244,8 +251,14 @@ async def to_code(config):
|
||||
is_at_least_very_verbose = this_severity >= very_verbose_severity
|
||||
has_serial_logging = baud_rate != 0
|
||||
|
||||
if CORE.is_esp8266 and has_serial_logging and is_at_least_verbose:
|
||||
debug_serial_port = HARDWARE_UART_TO_SERIAL[config.get(CONF_HARDWARE_UART)]
|
||||
if (
|
||||
(CORE.is_esp8266 or CORE.is_rp2040)
|
||||
and has_serial_logging
|
||||
and is_at_least_verbose
|
||||
):
|
||||
debug_serial_port = HARDWARE_UART_TO_SERIAL[CORE.target_platform][
|
||||
config.get(CONF_HARDWARE_UART)
|
||||
]
|
||||
cg.add_build_flag(f"-DDEBUG_ESP_PORT={debug_serial_port}")
|
||||
cg.add_build_flag("-DLWIP_DEBUG")
|
||||
DEBUG_COMPONENTS = {
|
||||
|
@@ -126,6 +126,7 @@ MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
|
||||
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
|
||||
MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent)
|
||||
MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent)
|
||||
MQTTUpdateComponent = mqtt_ns.class_("MQTTUpdateComponent", MQTTComponent)
|
||||
MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent)
|
||||
|
||||
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator")
|
||||
|
@@ -410,7 +410,10 @@ void MQTTClientComponent::subscribe(const std::string &topic, mqtt_callback_t ca
|
||||
|
||||
void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) {
|
||||
auto f = [callback](const std::string &topic, const std::string &payload) {
|
||||
json::parse_json(payload, [topic, callback](JsonObject root) { callback(topic, root); });
|
||||
json::parse_json(payload, [topic, callback](JsonObject root) -> bool {
|
||||
callback(topic, root);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
MQTTSubscription subscription{
|
||||
.topic = topic,
|
||||
|
@@ -137,6 +137,7 @@ constexpr const char *const MQTT_PAYLOAD_CLOSE = "pl_cls";
|
||||
constexpr const char *const MQTT_PAYLOAD_DISARM = "pl_disarm";
|
||||
constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd";
|
||||
constexpr const char *const MQTT_PAYLOAD_HOME = "pl_home";
|
||||
constexpr const char *const MQTT_PAYLOAD_INSTALL = "pl_inst";
|
||||
constexpr const char *const MQTT_PAYLOAD_LOCATE = "pl_loc";
|
||||
constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock";
|
||||
constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd";
|
||||
@@ -396,6 +397,7 @@ constexpr const char *const MQTT_PAYLOAD_CLOSE = "payload_close";
|
||||
constexpr const char *const MQTT_PAYLOAD_DISARM = "payload_disarm";
|
||||
constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed";
|
||||
constexpr const char *const MQTT_PAYLOAD_HOME = "payload_home";
|
||||
constexpr const char *const MQTT_PAYLOAD_INSTALL = "payload_install";
|
||||
constexpr const char *const MQTT_PAYLOAD_LOCATE = "payload_locate";
|
||||
constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock";
|
||||
constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed";
|
||||
|
62
esphome/components/mqtt/mqtt_update.cpp
Normal file
62
esphome/components/mqtt/mqtt_update.cpp
Normal 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
|
41
esphome/components/mqtt/mqtt_update.h
Normal file
41
esphome/components/mqtt/mqtt_update.h
Normal 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
|
40
esphome/components/one_wire/__init__.py
Normal file
40
esphome/components/one_wire/__init__.py
Normal 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))
|
40
esphome/components/one_wire/one_wire.cpp
Normal file
40
esphome/components/one_wire/one_wire.cpp
Normal 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
|
44
esphome/components/one_wire/one_wire.h
Normal file
44
esphome/components/one_wire/one_wire.h
Normal 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
|
88
esphome/components/one_wire/one_wire_bus.cpp
Normal file
88
esphome/components/one_wire/one_wire_bus.cpp
Normal 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
|
61
esphome/components/one_wire/one_wire_bus.h
Normal file
61
esphome/components/one_wire/one_wire_bus.h
Normal 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
|
@@ -16,6 +16,7 @@ from esphome import automation
|
||||
|
||||
CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"]
|
||||
|
||||
CONF_BOOT_IS_GOOD_AFTER = "boot_is_good_after"
|
||||
CONF_ON_SAFE_MODE = "on_safe_mode"
|
||||
|
||||
safe_mode_ns = cg.esphome_ns.namespace("safe_mode")
|
||||
@@ -34,6 +35,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SafeModeComponent),
|
||||
cv.Optional(
|
||||
CONF_BOOT_IS_GOOD_AFTER, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_DISABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
|
||||
cv.Optional(
|
||||
@@ -63,7 +67,9 @@ async def to_code(config):
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
condition = var.should_enter_safe_mode(
|
||||
config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT]
|
||||
config[CONF_NUM_ATTEMPTS],
|
||||
config[CONF_REBOOT_TIMEOUT],
|
||||
config[CONF_BOOT_IS_GOOD_AFTER],
|
||||
)
|
||||
cg.add(RawExpression(f"if ({condition}) return"))
|
||||
CORE.data[CONF_SAFE_MODE] = {}
|
||||
|
@@ -16,6 +16,8 @@ static const char *const TAG = "safe_mode";
|
||||
|
||||
void SafeModeComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Safe Mode:");
|
||||
ESP_LOGCONFIG(TAG, " Boot considered successful after %" PRIu32 " seconds",
|
||||
this->safe_mode_boot_is_good_after_ / 1000); // because milliseconds
|
||||
ESP_LOGCONFIG(TAG, " Invoke after %u boot attempts", this->safe_mode_num_attempts_);
|
||||
ESP_LOGCONFIG(TAG, " Remain in safe mode for %" PRIu32 " seconds",
|
||||
this->safe_mode_enable_time_ / 1000); // because milliseconds
|
||||
@@ -34,7 +36,7 @@ void SafeModeComponent::dump_config() {
|
||||
float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void SafeModeComponent::loop() {
|
||||
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
|
||||
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
// successful boot, reset counter
|
||||
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
|
||||
this->clean_rtc();
|
||||
@@ -60,9 +62,11 @@ bool SafeModeComponent::get_safe_mode_pending() {
|
||||
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
|
||||
}
|
||||
|
||||
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
|
||||
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time,
|
||||
uint32_t boot_is_good_after) {
|
||||
this->safe_mode_start_time_ = millis();
|
||||
this->safe_mode_enable_time_ = enable_time;
|
||||
this->safe_mode_boot_is_good_after_ = boot_is_good_after;
|
||||
this->safe_mode_num_attempts_ = num_attempts;
|
||||
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
|
||||
this->safe_mode_rtc_value_ = this->read_rtc_();
|
||||
|
@@ -11,7 +11,7 @@ namespace safe_mode {
|
||||
/// SafeModeComponent provides a safe way to recover from repeated boot failures
|
||||
class SafeModeComponent : public Component {
|
||||
public:
|
||||
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time);
|
||||
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after);
|
||||
|
||||
/// Set to true if the next startup will enter safe mode
|
||||
void set_safe_mode_pending(const bool &pending);
|
||||
@@ -33,11 +33,12 @@ class SafeModeComponent : public Component {
|
||||
void write_rtc_(uint32_t val);
|
||||
uint32_t read_rtc_();
|
||||
|
||||
bool boot_successful_{false}; ///< set to true after boot is considered successful
|
||||
uint32_t safe_mode_start_time_; ///< stores when safe mode was enabled
|
||||
uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for
|
||||
uint32_t safe_mode_rtc_value_;
|
||||
uint8_t safe_mode_num_attempts_;
|
||||
bool boot_successful_{false}; ///< set to true after boot is considered successful
|
||||
uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful
|
||||
uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for
|
||||
uint32_t safe_mode_rtc_value_{0};
|
||||
uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled
|
||||
uint8_t safe_mode_num_attempts_{0};
|
||||
ESPPreferenceObject rtc_;
|
||||
CallbackManager<void()> safe_mode_callback_{};
|
||||
|
||||
|
1
esphome/components/sdl/__init__.py
Normal file
1
esphome/components/sdl/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
72
esphome/components/sdl/display.py
Normal file
72
esphome/components/sdl/display.py
Normal 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_))
|
96
esphome/components/sdl/sdl_esphome.cpp
Normal file
96
esphome/components/sdl/sdl_esphome.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#ifdef USE_HOST
|
||||
#include "sdl_esphome.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sdl {
|
||||
|
||||
void Sdl::setup() {
|
||||
ESP_LOGD(TAG, "Starting setup");
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
this->window_ = SDL_CreateWindow(App.get_name().c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||
this->width_, this->height_, 0);
|
||||
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
|
||||
this->texture_ =
|
||||
SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
|
||||
SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND);
|
||||
ESP_LOGD(TAG, "Setup Complete");
|
||||
}
|
||||
void Sdl::update() {
|
||||
this->do_update_();
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_))
|
||||
return;
|
||||
SDL_Rect rect{this->x_low_, this->y_low_, this->x_high_ + 1 - this->x_low_, this->y_high_ + 1 - this->y_low_};
|
||||
this->x_low_ = this->width_;
|
||||
this->y_low_ = this->height_;
|
||||
this->x_high_ = 0;
|
||||
this->y_high_ = 0;
|
||||
SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
|
||||
SDL_RenderPresent(this->renderer_);
|
||||
}
|
||||
|
||||
void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
SDL_Rect rect{x_start, y_start, w, h};
|
||||
if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
|
||||
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
} else {
|
||||
auto stride = x_offset + w + x_pad;
|
||||
auto data = ptr + (stride * y_offset + x_offset) * 2;
|
||||
SDL_UpdateTexture(this->texture_, &rect, data, stride * 2);
|
||||
}
|
||||
SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
|
||||
SDL_RenderPresent(this->renderer_);
|
||||
}
|
||||
|
||||
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
SDL_Rect rect{x, y, 1, 1};
|
||||
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
|
||||
SDL_UpdateTexture(this->texture_, &rect, &data, 2);
|
||||
if (x < this->x_low_)
|
||||
this->x_low_ = x;
|
||||
if (y < this->y_low_)
|
||||
this->y_low_ = y;
|
||||
if (x > this->x_high_)
|
||||
this->x_high_ = x;
|
||||
if (y > this->y_high_)
|
||||
this->y_high_ = y;
|
||||
}
|
||||
|
||||
void Sdl::loop() {
|
||||
SDL_Event e;
|
||||
if (SDL_PollEvent(&e)) {
|
||||
switch (e.type) {
|
||||
case SDL_QUIT:
|
||||
exit(0);
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (e.button.button == 1) {
|
||||
this->mouse_x = e.button.x;
|
||||
this->mouse_y = e.button.y;
|
||||
this->mouse_down = e.button.state != 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
if (e.motion.state & 1) {
|
||||
this->mouse_x = e.button.x;
|
||||
this->mouse_y = e.button.y;
|
||||
this->mouse_down = true;
|
||||
} else {
|
||||
this->mouse_down = false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGV(TAG, "Event %d", e.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl
|
||||
} // namespace esphome
|
||||
#endif
|
54
esphome/components/sdl/sdl_esphome.h
Normal file
54
esphome/components/sdl/sdl_esphome.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#define SDL_MAIN_HANDLED
|
||||
#include "SDL.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sdl {
|
||||
|
||||
constexpr static const char *const TAG = "sdl";
|
||||
|
||||
class Sdl : public display::Display {
|
||||
public:
|
||||
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void set_dimensions(uint16_t width, uint16_t height) {
|
||||
this->width_ = width;
|
||||
this->height_ = height;
|
||||
}
|
||||
int get_width() override { return this->width_; }
|
||||
int get_height() override { return this->height_; }
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
void dump_config() override { LOG_DISPLAY("", "SDL", this); }
|
||||
|
||||
int mouse_x{};
|
||||
int mouse_y{};
|
||||
bool mouse_down{};
|
||||
|
||||
protected:
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
int width_{};
|
||||
int height_{};
|
||||
SDL_Renderer *renderer_{};
|
||||
SDL_Window *window_{};
|
||||
SDL_Texture *texture_{};
|
||||
uint16_t x_low_{0};
|
||||
uint16_t y_low_{0};
|
||||
uint16_t x_high_{0};
|
||||
uint16_t y_high_{0};
|
||||
};
|
||||
} // namespace sdl
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
22
esphome/components/sdl/touchscreen/__init__.py
Normal file
22
esphome/components/sdl/touchscreen/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
from esphome.components import touchscreen
|
||||
from ..display import Sdl, sdl_ns, CONF_SDL_ID
|
||||
|
||||
SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SdlTouchscreen),
|
||||
cv.GenerateID(CONF_SDL_ID): cv.use_id(Sdl),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_parented(var, config[CONF_SDL_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
26
esphome/components/sdl/touchscreen/sdl_touchscreen.h
Normal file
26
esphome/components/sdl/touchscreen/sdl_touchscreen.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "../sdl_esphome.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace sdl {
|
||||
|
||||
class SdlTouchscreen : public touchscreen::Touchscreen, public Parented<Sdl> {
|
||||
public:
|
||||
void setup() override {
|
||||
this->x_raw_max_ = this->display_->get_width();
|
||||
this->y_raw_max_ = this->display_->get_height();
|
||||
}
|
||||
|
||||
void update_touches() override {
|
||||
if (this->parent_->mouse_down) {
|
||||
add_raw_touch_position_(0, this->parent_->mouse_x, this->parent_->mouse_y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sdl
|
||||
} // namespace esphome
|
||||
#endif
|
108
esphome/components/update/__init__.py
Normal file
108
esphome/components/update/__init__.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from esphome import automation
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_WEB_SERVER_ID,
|
||||
DEVICE_CLASS_FIRMWARE,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
update_ns = cg.esphome_ns.namespace("update")
|
||||
UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase)
|
||||
|
||||
UpdateInfo = update_ns.struct("UpdateInfo")
|
||||
|
||||
PerformAction = update_ns.class_("PerformAction", automation.Action)
|
||||
IsAvailableCondition = update_ns.class_("IsAvailableCondition", automation.Condition)
|
||||
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_FIRMWARE,
|
||||
]
|
||||
|
||||
CONF_ON_UPDATE_AVAILABLE = "on_update_available"
|
||||
|
||||
UPDATE_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTUpdateComponent),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_ON_UPDATE_AVAILABLE): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def setup_update_core_(var, config):
|
||||
await setup_entity(var, config)
|
||||
|
||||
if device_class_config := config.get(CONF_DEVICE_CLASS):
|
||||
cg.add(var.set_device_class(device_class_config))
|
||||
|
||||
if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE):
|
||||
await automation.build_automation(
|
||||
var.get_update_available_trigger(),
|
||||
[(UpdateInfo.operator("ref").operator("const"), "x")],
|
||||
on_update_available,
|
||||
)
|
||||
|
||||
if mqtt_id_config := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id_config, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
if web_server_id_config := config.get(CONF_WEB_SERVER_ID):
|
||||
web_server_ = cg.get_variable(web_server_id_config)
|
||||
web_server.add_entity_to_sorting_list(web_server_, var, config)
|
||||
|
||||
|
||||
async def register_update(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_update(var))
|
||||
await setup_update_core_(var, config)
|
||||
|
||||
|
||||
async def new_update(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await register_update(var, config)
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_UPDATE")
|
||||
cg.add_global(update_ns.using)
|
||||
|
||||
|
||||
UPDATE_AUTOMATION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(UpdateEntity),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("update.perform", PerformAction, UPDATE_AUTOMATION_SCHEMA)
|
||||
async def update_perform_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, paren, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"update.is_available", IsAvailableCondition, UPDATE_AUTOMATION_SCHEMA
|
||||
)
|
||||
async def update_is_available_condition_to_code(
|
||||
config, condition_id, template_arg, args
|
||||
):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, paren, paren)
|
12
esphome/components/update/update_entity.cpp
Normal file
12
esphome/components/update/update_entity.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "update_entity.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace update {
|
||||
|
||||
void UpdateEntity::publish_state() {
|
||||
this->has_state_ = true;
|
||||
this->state_callback_.call();
|
||||
}
|
||||
|
||||
} // namespace update
|
||||
} // namespace esphome
|
51
esphome/components/update/update_entity.h
Normal file
51
esphome/components/update/update_entity.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace update {
|
||||
|
||||
struct UpdateInfo {
|
||||
std::string latest_version;
|
||||
std::string current_version;
|
||||
std::string title;
|
||||
std::string summary;
|
||||
std::string release_url;
|
||||
std::string firmware_url;
|
||||
std::string md5;
|
||||
bool has_progress{false};
|
||||
float progress;
|
||||
};
|
||||
|
||||
enum UpdateState : uint8_t {
|
||||
UPDATE_STATE_UNKNOWN,
|
||||
UPDATE_STATE_NO_UPDATE,
|
||||
UPDATE_STATE_AVAILABLE,
|
||||
UPDATE_STATE_INSTALLING,
|
||||
};
|
||||
|
||||
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
|
||||
public:
|
||||
bool has_state() const { return this->has_state_; }
|
||||
|
||||
void publish_state();
|
||||
|
||||
virtual void perform() = 0;
|
||||
|
||||
const UpdateInfo &update_info = update_info_;
|
||||
const UpdateState &state = state_;
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
|
||||
protected:
|
||||
UpdateState state_{UPDATE_STATE_UNKNOWN};
|
||||
UpdateInfo update_info_;
|
||||
bool has_state_{false};
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
};
|
||||
|
||||
} // namespace update
|
||||
} // namespace esphome
|
@@ -428,14 +428,15 @@ void VoiceAssistant::loop() {
|
||||
#ifdef USE_SPEAKER
|
||||
void VoiceAssistant::write_speaker_() {
|
||||
if (this->speaker_buffer_size_ > 0) {
|
||||
size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_);
|
||||
size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
|
||||
size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
|
||||
if (written > 0) {
|
||||
memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
|
||||
this->speaker_buffer_size_ -= written;
|
||||
this->speaker_buffer_index_ -= written;
|
||||
this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); });
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Speaker buffer full, trying again next loop");
|
||||
ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,7 +799,7 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
|
||||
this->speaker_buffer_index_ += msg.data.length();
|
||||
this->speaker_buffer_size_ += msg.data.length();
|
||||
this->speaker_bytes_received_ += msg.data.length();
|
||||
ESP_LOGD(TAG, "Received audio: %d bytes from API", msg.data.length());
|
||||
ESP_LOGV(TAG, "Received audio: %" PRId32 " bytes from API", msg.data.length());
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
|
||||
}
|
||||
|
@@ -94,6 +94,9 @@ WaveshareEPaper2P13InV2 = waveshare_epaper_ns.class_(
|
||||
WaveshareEPaper2P13InV3 = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper2P13InV3", WaveshareEPaper
|
||||
)
|
||||
WaveshareEPaper13P3InK = waveshare_epaper_ns.class_(
|
||||
"WaveshareEPaper13P3InK", WaveshareEPaper
|
||||
)
|
||||
GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper)
|
||||
|
||||
WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel")
|
||||
@@ -133,6 +136,7 @@ MODELS = {
|
||||
"2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE),
|
||||
"2.13inv3": ("c", WaveshareEPaper2P13InV3),
|
||||
"1.54in-m5coreink-m09": ("c", GDEW0154M09),
|
||||
"13.3in-k": ("b", WaveshareEPaper13P3InK),
|
||||
}
|
||||
|
||||
RESET_PIN_REQUIRED_MODELS = ("2.13inv2", "2.13in-ttgo-b74")
|
||||
|
@@ -2963,5 +2963,88 @@ void WaveshareEPaper2P13InDKE::set_full_update_every(uint32_t full_update_every)
|
||||
this->full_update_every_ = full_update_every;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// 13.3in (K version)
|
||||
// Datasheet/Specification/Reference:
|
||||
// - https://files.waveshare.com/wiki/13.3inch-e-Paper-HAT-(K)/13.3-inch-e-Paper-(K)-user-manual.pdf
|
||||
// - https://github.com/waveshareteam/e-Paper/tree/master/Arduino/epd13in3k
|
||||
// ========================================================
|
||||
|
||||
// using default wait_until_idle_() function
|
||||
void WaveshareEPaper13P3InK::initialize() {
|
||||
this->wait_until_idle_();
|
||||
this->command(0x12); // SWRESET
|
||||
this->wait_until_idle_();
|
||||
|
||||
this->command(0x0c); // set soft start
|
||||
this->data(0xae);
|
||||
this->data(0xc7);
|
||||
this->data(0xc3);
|
||||
this->data(0xc0);
|
||||
this->data(0x80);
|
||||
|
||||
this->command(0x01); // driver output control
|
||||
this->data((get_height_internal() - 1) % 256); // Y
|
||||
this->data((get_height_internal() - 1) / 256); // Y
|
||||
this->data(0x00);
|
||||
|
||||
this->command(0x11); // data entry mode
|
||||
this->data(0x03);
|
||||
|
||||
// SET WINDOWS
|
||||
// XRAM_START_AND_END_POSITION
|
||||
this->command(0x44);
|
||||
this->data(0 & 0xFF);
|
||||
this->data((0 >> 8) & 0x03);
|
||||
this->data((get_width_internal() - 1) & 0xFF);
|
||||
this->data(((get_width_internal() - 1) >> 8) & 0x03);
|
||||
// YRAM_START_AND_END_POSITION
|
||||
this->command(0x45);
|
||||
this->data(0 & 0xFF);
|
||||
this->data((0 >> 8) & 0x03);
|
||||
this->data((get_height_internal() - 1) & 0xFF);
|
||||
this->data(((get_height_internal() - 1) >> 8) & 0x03);
|
||||
|
||||
this->command(0x3C); // Border setting
|
||||
this->data(0x01);
|
||||
|
||||
this->command(0x18); // use the internal temperature sensor
|
||||
this->data(0x80);
|
||||
|
||||
// SET CURSOR
|
||||
// XRAM_ADDRESS
|
||||
this->command(0x4E);
|
||||
this->data(0 & 0xFF);
|
||||
this->data((0 >> 8) & 0x03);
|
||||
// YRAM_ADDRESS
|
||||
this->command(0x4F);
|
||||
this->data(0 & 0xFF);
|
||||
this->data((0 >> 8) & 0x03);
|
||||
}
|
||||
void HOT WaveshareEPaper13P3InK::display() {
|
||||
// do single full update
|
||||
this->command(0x24);
|
||||
this->start_data_();
|
||||
this->write_array(this->buffer_, this->get_buffer_length_());
|
||||
this->end_data_();
|
||||
|
||||
// COMMAND DISPLAY REFRESH
|
||||
this->command(0x22);
|
||||
this->data(0xF7);
|
||||
this->command(0x20);
|
||||
}
|
||||
|
||||
int WaveshareEPaper13P3InK::get_width_internal() { return 960; }
|
||||
int WaveshareEPaper13P3InK::get_height_internal() { return 680; }
|
||||
uint32_t WaveshareEPaper13P3InK::idle_timeout_() { return 10000; }
|
||||
void WaveshareEPaper13P3InK::dump_config() {
|
||||
LOG_DISPLAY("", "Waveshare E-Paper", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 13.3inK");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace waveshare_epaper
|
||||
} // namespace esphome
|
||||
|
@@ -163,6 +163,7 @@ enum WaveshareEPaperTypeBModel {
|
||||
WAVESHARE_EPAPER_7_5_IN,
|
||||
WAVESHARE_EPAPER_7_5_INV2,
|
||||
WAVESHARE_EPAPER_7_5_IN_B_V2,
|
||||
WAVESHARE_EPAPER_13_3_IN_K,
|
||||
};
|
||||
|
||||
class WaveshareEPaper2P7In : public WaveshareEPaper {
|
||||
@@ -769,5 +770,28 @@ class WaveshareEPaper2P13InV3 : public WaveshareEPaper {
|
||||
bool is_busy_{false};
|
||||
void write_lut_(const uint8_t *lut);
|
||||
};
|
||||
|
||||
class WaveshareEPaper13P3InK : public WaveshareEPaper {
|
||||
public:
|
||||
void initialize() override;
|
||||
|
||||
void display() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void deep_sleep() override {
|
||||
// COMMAND DEEP SLEEP
|
||||
this->command(0x10);
|
||||
this->data(0x01);
|
||||
}
|
||||
|
||||
protected:
|
||||
int get_width_internal() override;
|
||||
|
||||
int get_height_internal() override;
|
||||
|
||||
uint32_t idle_timeout_() override;
|
||||
};
|
||||
|
||||
} // namespace waveshare_epaper
|
||||
} // namespace esphome
|
||||
|
@@ -177,5 +177,14 @@ bool ListEntitiesIterator::on_event(event::Event *event) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) {
|
||||
if (this->web_server_->events_.count() == 0)
|
||||
return true;
|
||||
this->web_server_->events_.send(this->web_server_->update_json(update, DETAIL_ALL).c_str(), "state");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace web_server
|
||||
} // namespace esphome
|
||||
|
@@ -68,6 +68,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *update) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
WebServer *web_server_;
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user